Basic functionallity
This commit is contained in:
30
hextool.py
Executable file
30
hextool.py
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
|
||||
|
||||
def toBinary(value):
|
||||
s = ""
|
||||
v = 128
|
||||
for i in range(8):
|
||||
if value & v:
|
||||
s += "1"
|
||||
else:
|
||||
s += "0"
|
||||
v /= 2
|
||||
return s
|
||||
|
||||
binstr = ""
|
||||
for n in range(1, len(sys.argv)):
|
||||
try:
|
||||
value = int(sys.argv[n])
|
||||
except ValueError:
|
||||
value = int(sys.argv[n], 16)
|
||||
bin = toBinary(value)
|
||||
print "%3d = 0x%02x = %sb" % (value, value, bin),
|
||||
if (31 < value and value < 255):
|
||||
print " = %c" % value
|
||||
else:
|
||||
print ""
|
||||
binstr += bin + " "
|
||||
|
||||
print binstr
|
||||
186
pom.xml
Executable file
186
pom.xml
Executable file
@@ -0,0 +1,186 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>se.slackers.locality</groupId>
|
||||
<artifactId>locality</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<version>1.0</version>
|
||||
<name>locality</name>
|
||||
<description>locality</description>
|
||||
<url>http://slackers.se/app/locality</url>
|
||||
|
||||
<!-- DEVELOPERS -->
|
||||
<developers>
|
||||
<developer>
|
||||
<id>eb</id>
|
||||
<name>
|
||||
Erik Byström
|
||||
</name>
|
||||
<email>erik.bystrom+lastfmsaver@gmail.com</email>
|
||||
<roles>
|
||||
<role>Developer</role>
|
||||
</roles>
|
||||
<organization>slackers.se</organization>
|
||||
<timezone>+1</timezone>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<!-- DEPENDENCIES -->
|
||||
<dependencies>
|
||||
<!-- SPRING -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring</artifactId>
|
||||
<version>2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DATABASE -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>1.0.67</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HIBERNATE -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate</artifactId>
|
||||
<version>3.2.6.ga</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-commons-annotations</artifactId>
|
||||
<version>3.3.0.ga</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-annotations</artifactId>
|
||||
<version>3.3.0.ga</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-hibernate3</artifactId>
|
||||
<version>2.0.8</version>
|
||||
</dependency>
|
||||
|
||||
<!-- COMMONS -->
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-digester</groupId>
|
||||
<artifactId>commons-digester</artifactId>
|
||||
<version>1.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-dbcp</groupId>
|
||||
<artifactId>commons-dbcp</artifactId>
|
||||
<version>1.2.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JDOM -->
|
||||
<dependency>
|
||||
<groupId>jdom</groupId>
|
||||
<artifactId>jdom</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jaxen</groupId>
|
||||
<artifactId>jaxen</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JGoodies -->
|
||||
<dependency>
|
||||
<groupId>jgoodies</groupId>
|
||||
<artifactId>forms</artifactId>
|
||||
<version>1.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.swinglabs</groupId>
|
||||
<artifactId>swingx</artifactId>
|
||||
<version>0.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Http Client -->
|
||||
<dependency>
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<version>3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TESTING -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>2.5</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-eclipse-plugin</artifactId>
|
||||
<configuration>
|
||||
<downloadSources>true</downloadSources>
|
||||
<downloadJavadocs>true</downloadJavadocs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.5</source>
|
||||
<target>1.5</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-report-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jxr-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<configuration>
|
||||
<links>
|
||||
<link>
|
||||
http://java.sun.com/j2ee/1.4/docs/api
|
||||
</link>
|
||||
<link>
|
||||
http://java.sun.com/j2se/1.5.0/docs/api
|
||||
</link>
|
||||
</links>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>
|
||||
maven-project-info-reports-plugin
|
||||
</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
</project>
|
||||
20
src/main/java/se/slackers/jss/mediastream/MediaStream.java
Normal file
20
src/main/java/se/slackers/jss/mediastream/MediaStream.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package se.slackers.jss.mediastream;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* BaseClass for classes that stream audio data.
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
abstract public class MediaStream {
|
||||
private InputStream inputStream;
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public void setInputStream(InputStream inputStream) {
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
}
|
||||
48
src/main/java/se/slackers/locality/dao/MetaTagDao.java
Executable file
48
src/main/java/se/slackers/locality/dao/MetaTagDao.java
Executable file
@@ -0,0 +1,48 @@
|
||||
package se.slackers.locality.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import se.slackers.locality.model.MetaTag;
|
||||
|
||||
|
||||
public interface MetaTagDao {
|
||||
|
||||
|
||||
/**
|
||||
* Removes the metatag from the database and deletes all references to it.
|
||||
* @param tag
|
||||
*/
|
||||
public void delete(MetaTag tag);
|
||||
|
||||
/**
|
||||
* Find a metatag by the metatag id. If no metatag was found a DataRetrievalFailureException is thrown.
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public MetaTag get(Long id);
|
||||
|
||||
/**
|
||||
* Find a metatag by the name of the metatag. If no metatag was found a DataRetrievalFailureException is thrown.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public MetaTag get(String name);
|
||||
|
||||
/**
|
||||
* Searches for metatags that have similar names to the given string. Before the search the name is converted to
|
||||
* lower case.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public List<MetaTag> getLike(String name);
|
||||
|
||||
/**
|
||||
* Saves or updates the metatag.
|
||||
*
|
||||
* @param metatag
|
||||
*/
|
||||
public void save(MetaTag metatag);
|
||||
}
|
||||
45
src/main/java/se/slackers/locality/dao/TagDao.java
Executable file
45
src/main/java/se/slackers/locality/dao/TagDao.java
Executable file
@@ -0,0 +1,45 @@
|
||||
package se.slackers.locality.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import se.slackers.locality.model.Tag;
|
||||
|
||||
public interface TagDao {
|
||||
/**
|
||||
* Removes the tag from the database and deletes all references to it.
|
||||
* @param tag
|
||||
*/
|
||||
public void delete(Tag tag);
|
||||
|
||||
/**
|
||||
* Find a tag by the tag id. If no tag was found a DataRetrievalFailureException is thrown.
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public Tag get(Long id);
|
||||
|
||||
/**
|
||||
* Find a tag by the name of the tag. If no tag was found a DataRetrievalFailureException is thrown.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public Tag get(String name);
|
||||
|
||||
/**
|
||||
* Searches for tags that have similar names to the given string. Before the search the name is converted to
|
||||
* lower case.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public List<Tag> getLike(String name);
|
||||
|
||||
/**
|
||||
* Saves or updates the tag.
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
public void save(Tag tag);
|
||||
}
|
||||
64
src/main/java/se/slackers/locality/dao/hibernate/MetaTagDaoImpl.java
Executable file
64
src/main/java/se/slackers/locality/dao/hibernate/MetaTagDaoImpl.java
Executable file
@@ -0,0 +1,64 @@
|
||||
package se.slackers.locality.dao.hibernate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
|
||||
|
||||
import se.slackers.locality.dao.MetaTagDao;
|
||||
import se.slackers.locality.model.MetaTag;
|
||||
|
||||
public class MetaTagDaoImpl extends HibernateDaoSupport implements MetaTagDao {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void delete(MetaTag tag) {
|
||||
getHibernateTemplate().delete(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public MetaTag get(Long id) {
|
||||
List<MetaTag> result = (List<MetaTag>)getHibernateTemplate().find("from MetaTag tag fetch all properties where tag.id=?", id);
|
||||
|
||||
if (result.isEmpty())
|
||||
throw new DataRetrievalFailureException("No metatag with id "+id+" could be found");
|
||||
|
||||
assert result.size() == 1 : "More than one metatag found with id "+id;
|
||||
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public MetaTag get(String name) {
|
||||
List<MetaTag> result = (List<MetaTag>)getHibernateTemplate().find("from MetaTag tag fetch all properties where tag.name=?", name);
|
||||
|
||||
if (result.isEmpty())
|
||||
throw new DataRetrievalFailureException("No metatag with name "+name+" could be found");
|
||||
|
||||
assert result.size() == 1 : "More than one metatag found with name "+name;
|
||||
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<MetaTag> getLike(String name) {
|
||||
return (List<MetaTag>)getHibernateTemplate().find("from MetaTag tag fetch all properties where lower(tag.name) like ? order by tag.name", name.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void save(MetaTag tag) {
|
||||
getHibernateTemplate().saveOrUpdate(tag);
|
||||
}
|
||||
}
|
||||
64
src/main/java/se/slackers/locality/dao/hibernate/TagDaoImpl.java
Executable file
64
src/main/java/se/slackers/locality/dao/hibernate/TagDaoImpl.java
Executable file
@@ -0,0 +1,64 @@
|
||||
package se.slackers.locality.dao.hibernate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
|
||||
|
||||
import se.slackers.locality.dao.TagDao;
|
||||
import se.slackers.locality.model.Tag;
|
||||
|
||||
public class TagDaoImpl extends HibernateDaoSupport implements TagDao {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void delete(Tag tag) {
|
||||
getHibernateTemplate().delete(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Tag get(Long id) {
|
||||
List<Tag> result = (List<Tag>)getHibernateTemplate().find("from Tag tag fetch all properties where tag.id=?", id);
|
||||
|
||||
if (result.isEmpty())
|
||||
throw new DataRetrievalFailureException("No tag with id "+id+" could be found");
|
||||
|
||||
assert result.size() == 1 : "More than one tag found with id "+id;
|
||||
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Tag get(String name) {
|
||||
List<Tag> result = (List<Tag>)getHibernateTemplate().find("from Tag tag fetch all properties where tag.name=?", name);
|
||||
|
||||
if (result.isEmpty())
|
||||
throw new DataRetrievalFailureException("No tag with name "+name+" could be found");
|
||||
|
||||
assert result.size() == 1 : "More than one tag found with name "+name;
|
||||
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Tag> getLike(String name) {
|
||||
return (List<Tag>)getHibernateTemplate().find("from Tag tag fetch all properties where lower(tag.name) like ? order by tag.name", name.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void save(Tag tag) {
|
||||
getHibernateTemplate().saveOrUpdate(tag);
|
||||
}
|
||||
}
|
||||
115
src/main/java/se/slackers/locality/data/CircularBuffer.java
Normal file
115
src/main/java/se/slackers/locality/data/CircularBuffer.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class CircularBuffer {
|
||||
private ByteBuffer buffer;
|
||||
private int readIndex = 0;
|
||||
private int writeIndex = 0;
|
||||
|
||||
public CircularBuffer(int bufferSize) {
|
||||
buffer = ByteBuffer.allocateDirect(bufferSize);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
readIndex = 0;
|
||||
writeIndex = 0;
|
||||
}
|
||||
|
||||
public int read(byte [] dest, int offset, int length) {
|
||||
assert length < buffer.capacity() : "The requested read is bigger than the buffer";
|
||||
|
||||
if (writeIndex == readIndex) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
buffer.position(readIndex);
|
||||
if (writeIndex < readIndex) {
|
||||
int remainder = buffer.remaining();
|
||||
if (remainder < length) {
|
||||
buffer.get(dest, offset, remainder);
|
||||
|
||||
offset += remainder;
|
||||
length -= remainder;
|
||||
|
||||
readIndex = 0;
|
||||
buffer.position(readIndex);
|
||||
|
||||
int space = writeIndex-readIndex;
|
||||
if (space <= length) {
|
||||
length = space;
|
||||
}
|
||||
|
||||
buffer.get(dest, offset, length);
|
||||
readIndex += length;
|
||||
|
||||
return remainder + length;
|
||||
} else {
|
||||
buffer.get(dest, offset, remainder);
|
||||
readIndex += remainder;
|
||||
return remainder;
|
||||
}
|
||||
} else {
|
||||
int space = writeIndex - readIndex;
|
||||
if (space <= length) {
|
||||
length = space;
|
||||
}
|
||||
|
||||
buffer.get(dest, offset, length);
|
||||
readIndex += length;
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean write(byte [] source, int offset, int length) {
|
||||
assert length < buffer.capacity() : "The requested write is bigger than the buffer";
|
||||
|
||||
buffer.position(writeIndex);
|
||||
|
||||
|
||||
if ( (readIndex <= writeIndex && writeIndex + length < buffer.capacity()) ||
|
||||
(writeIndex < readIndex && length < readIndex-writeIndex)) {
|
||||
// source fits in the remainder of the buffer
|
||||
buffer.put(source, offset, length);
|
||||
writeIndex += length;
|
||||
return true;
|
||||
} else {
|
||||
// the source don't fit in the buffer without wrapping
|
||||
int remainder = buffer.remaining();
|
||||
|
||||
if (readIndex < writeIndex && length > readIndex + remainder) {
|
||||
return false;
|
||||
}
|
||||
if (writeIndex < readIndex && length > readIndex-writeIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
buffer.put(source, offset, remainder);
|
||||
|
||||
offset += remainder;
|
||||
length -= remainder;
|
||||
|
||||
writeIndex = 0;
|
||||
buffer.position(writeIndex);
|
||||
|
||||
assert length < readIndex : "There is not enough room for this write operation";
|
||||
buffer.put(source, offset, length);
|
||||
writeIndex += length;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return writeIndex == readIndex;
|
||||
}
|
||||
|
||||
public boolean isFull() {
|
||||
if (writeIndex+1 <= buffer.capacity() && writeIndex+1 == readIndex)
|
||||
return true;
|
||||
if (writeIndex == buffer.capacity()-1 && readIndex == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import se.slackers.locality.exception.InvalidBufferPositionException;
|
||||
|
||||
public class ExpandOnWriteCircularBuffer {
|
||||
private ByteBuffer buffer;
|
||||
private int readOffset = 0;
|
||||
private int readIndex = 0;
|
||||
private int writeIndex = 0;
|
||||
|
||||
public ExpandOnWriteCircularBuffer(int bufferSize) {
|
||||
buffer = ByteBuffer.allocateDirect(bufferSize);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
readOffset = 0;
|
||||
readIndex = 0;
|
||||
writeIndex = 0;
|
||||
}
|
||||
|
||||
public synchronized int read(int position, byte [] dest, int destOffset, int destLength) {
|
||||
assert destLength < buffer.capacity() : "The requested read is bigger than the buffer";
|
||||
|
||||
// make sure the position is larger than the smallest buffer position
|
||||
if (position < readOffset) {
|
||||
throw new InvalidBufferPositionException("Read position "+position+" is smaller than ["+readOffset+"]");
|
||||
}
|
||||
// make sure the position is smaller then the smallest buffer position
|
||||
int offset = position - readOffset;
|
||||
if (offset > getBytesInBuffer()) {
|
||||
throw new InvalidBufferPositionException("Read position "+position+" is larger then ["+(readOffset+getBytesInBuffer())+"]");
|
||||
}
|
||||
|
||||
// check if the buffer is empty
|
||||
if (writeIndex == readIndex) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
buffer.position(offset);
|
||||
if (writeIndex < readIndex) {
|
||||
int remainder = buffer.remaining();
|
||||
if (remainder < destLength) {
|
||||
buffer.get(dest, destOffset, remainder);
|
||||
|
||||
destOffset += remainder;
|
||||
destLength -= remainder;
|
||||
|
||||
buffer.position(0);
|
||||
|
||||
int space = writeIndex-0;
|
||||
if (space <= destLength) {
|
||||
destLength = space;
|
||||
}
|
||||
|
||||
buffer.get(dest, destOffset, destLength);
|
||||
|
||||
return remainder + destLength;
|
||||
} else {
|
||||
buffer.get(dest, destOffset, remainder);
|
||||
return remainder;
|
||||
}
|
||||
} else {
|
||||
int space = writeIndex - offset;
|
||||
if (space <= destLength) {
|
||||
destLength = space;
|
||||
}
|
||||
|
||||
buffer.get(dest, destOffset, destLength);
|
||||
return destLength;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean write(byte [] source, int offset, int length) {
|
||||
assert length < buffer.capacity() : "The requested write is bigger than the buffer";
|
||||
|
||||
buffer.position(writeIndex);
|
||||
|
||||
if (length < buffer.capacity()-getBytesInBuffer()) {
|
||||
// the write fits in the buffer without changing the readIndex
|
||||
if (writeIndex <= readIndex) {
|
||||
|
||||
assert (readIndex-writeIndex) == (buffer.capacity()-getBytesInBuffer()) : "Buffer size is invalid";
|
||||
|
||||
buffer.put(source, offset, length);
|
||||
writeIndex += length;
|
||||
return true;
|
||||
} else {
|
||||
int remainder = buffer.remaining();
|
||||
if (length < remainder) {
|
||||
// the write fits in the remaining buffer
|
||||
buffer.put(source, offset, length);
|
||||
writeIndex += length;
|
||||
return true;
|
||||
} else {
|
||||
// the write needs to be wrapped
|
||||
buffer.put(source, offset, remainder);
|
||||
buffer.position(0);
|
||||
buffer.put(source, offset+remainder, length-remainder);
|
||||
writeIndex = length-remainder;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// readIndex needs to be changed after the write
|
||||
int remainder = buffer.remaining();
|
||||
if (length < remainder) {
|
||||
// the write fits in the remaining buffer
|
||||
buffer.put(source, offset, length);
|
||||
writeIndex += length;
|
||||
} else {
|
||||
// the write needs to be wrapped
|
||||
buffer.put(source, offset, remainder);
|
||||
buffer.position(0);
|
||||
buffer.put(source, offset+remainder, length-remainder);
|
||||
writeIndex = length-remainder;
|
||||
}
|
||||
|
||||
int oldRead = readIndex;
|
||||
readIndex = writeIndex + 1;
|
||||
if (readIndex >= buffer.capacity()) {
|
||||
readIndex -= buffer.capacity();
|
||||
}
|
||||
|
||||
// increase the offset index
|
||||
if (readIndex < oldRead) {
|
||||
readOffset += buffer.capacity()-oldRead + readIndex;
|
||||
} else {
|
||||
readOffset += readIndex-oldRead;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public int getReadOffset() {
|
||||
return readOffset;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return writeIndex == readIndex;
|
||||
}
|
||||
|
||||
public boolean isFull() {
|
||||
if (writeIndex+1 <= buffer.capacity() && writeIndex+1 == readIndex)
|
||||
return true;
|
||||
if (writeIndex == buffer.capacity()-1 && readIndex == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getBytesInBuffer() {
|
||||
if (writeIndex < readIndex) {
|
||||
return (buffer.capacity()-readIndex) + writeIndex;
|
||||
}
|
||||
return writeIndex-readIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.exception.FrameHasNotBeenLoadedException;
|
||||
import se.slackers.locality.exception.FrameIsTooOldException;
|
||||
import se.slackers.locality.exception.FrameStorageIsEmptyException;
|
||||
|
||||
/**
|
||||
* Threadsafe.
|
||||
*
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class FixedFrameSizeFrameStorage implements FrameStorage {
|
||||
private static final Logger log = Logger.getLogger(FixedFrameSizeFrameStorage.class);
|
||||
|
||||
private LinkedList<FrameStorageEntry> frames = new LinkedList<FrameStorageEntry>();
|
||||
private long frameLength = 26; // MP3 frame length
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized FrameStorageEntry find(long time) {
|
||||
if (frames.isEmpty()) {
|
||||
throw new FrameStorageIsEmptyException();
|
||||
}
|
||||
|
||||
long firstFrameTime = frames.getFirst().getStartTime();
|
||||
long lastFrameTime = frames.getLast().getStopTime();
|
||||
|
||||
// make sure the frame is within the represented interval
|
||||
if (lastFrameTime <= time) {
|
||||
//log.debug("Request: "+time+", LastFrame: "+lastFrameTime+", Diff: "+(time-lastFrameTime));
|
||||
throw new FrameHasNotBeenLoadedException();
|
||||
}
|
||||
|
||||
if (time < firstFrameTime) {
|
||||
throw new FrameIsTooOldException();
|
||||
}
|
||||
|
||||
int index = (int) ((time - firstFrameTime) / frameLength);
|
||||
|
||||
return frames.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized void add(FrameStorageEntry entry) {
|
||||
frames.add(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized void purgeUntil(long time) {
|
||||
//log.debug("Purging framestorage until "+time);
|
||||
|
||||
Iterator<FrameStorageEntry> iterator = frames.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getStopTime() <= time) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized long getFirstFrameTime() {
|
||||
if (frames.isEmpty()) {
|
||||
throw new FrameStorageIsEmptyException();
|
||||
}
|
||||
|
||||
return frames.getFirst().getStartTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized long getLastFrameTime() {
|
||||
if (frames.isEmpty()) {
|
||||
throw new FrameStorageIsEmptyException();
|
||||
}
|
||||
|
||||
return frames.getLast().getStopTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame length that is used by the instance.
|
||||
* @return The frame length in milliseconds
|
||||
*/
|
||||
public long getFrameLength() {
|
||||
return frameLength;
|
||||
}
|
||||
|
||||
public void setFrameLength(long frameLength) {
|
||||
this.frameLength = frameLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized void clear() {
|
||||
log.debug("Clearing frame storage");
|
||||
frames.clear();
|
||||
}
|
||||
}
|
||||
54
src/main/java/se/slackers/locality/data/FrameStorage.java
Normal file
54
src/main/java/se/slackers/locality/data/FrameStorage.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import se.slackers.locality.exception.FrameHasNotBeenLoadedException;
|
||||
|
||||
public interface FrameStorage {
|
||||
|
||||
/**
|
||||
* Adds a frame to the FrameStorage. This method only adds the frame to the
|
||||
* end of the storage. So adding out-of-order frames will cause error in
|
||||
* playback.
|
||||
*
|
||||
* @param entry
|
||||
*/
|
||||
public void add(FrameStorageEntry entry);
|
||||
|
||||
/**
|
||||
* Returns the frame that overlaps the given time. If the FrameStorage is
|
||||
* empty {@link FrameStorageIsEmptyException} is be thrown. If no frame
|
||||
* could be found for the specified time {@link FrameHasNotBeenLoadedException} or
|
||||
* {@link FrameIsTooOldException} is thrown.
|
||||
*
|
||||
* @param time
|
||||
* @return A FrameStorageEntry that overlapped the given time.
|
||||
*/
|
||||
public FrameStorageEntry find(long time);
|
||||
|
||||
/**
|
||||
* Removes all frames that doesn't overlap the given time.
|
||||
*
|
||||
* @param time
|
||||
*/
|
||||
public void purgeUntil(long time);
|
||||
|
||||
/**
|
||||
* Clears the frame storage.
|
||||
*/
|
||||
public void clear();
|
||||
|
||||
/**
|
||||
* Returns the start time of the first frame. If the storage is empty
|
||||
* {@link FrameStorageIsEmptyException} will be thrown.
|
||||
*
|
||||
* @return Start time of first frame.
|
||||
*/
|
||||
public long getFirstFrameTime();
|
||||
|
||||
/**
|
||||
* Returns the end time of the last frame. If the storage is empty
|
||||
* {@link FrameStorageIsEmptyException} will be thrown.
|
||||
*
|
||||
* @return End time of the last frame in storage.
|
||||
*/
|
||||
public long getLastFrameTime();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import se.slackers.locality.media.Frame;
|
||||
|
||||
/**
|
||||
* Immutable class that wraps a frame with start and stop times.
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class FrameStorageEntry implements Comparable<FrameStorageEntry> {
|
||||
private long startTime;
|
||||
private long stopTime;
|
||||
private Frame frame;
|
||||
|
||||
public FrameStorageEntry(long time, Frame frame) {
|
||||
this.startTime = time;
|
||||
this.stopTime = time + frame.getLength();
|
||||
this.frame = frame;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public long getStopTime() {
|
||||
return stopTime;
|
||||
}
|
||||
|
||||
public Frame getFrame() {
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation considers overlapping intervals to be equal.
|
||||
*/
|
||||
public int compareTo(FrameStorageEntry o) {
|
||||
if (stopTime < o.startTime)
|
||||
return -1;
|
||||
|
||||
if (startTime >= o.stopTime)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Spans from "+getStartTime()+" to "+getStopTime() +" ("+getFrame()+")";
|
||||
}
|
||||
}
|
||||
180
src/main/java/se/slackers/locality/data/MetadataManager.java
Normal file
180
src/main/java/se/slackers/locality/data/MetadataManager.java
Normal file
@@ -0,0 +1,180 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import se.slackers.locality.media.queue.MediaQueue;
|
||||
import se.slackers.locality.media.queue.MediaQueueProcessorListener;
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
import se.slackers.locality.model.MetadataType;
|
||||
|
||||
/**
|
||||
* Controls when a full metadata chunk should be rendered and in which format.
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class MetadataManager implements MediaQueueProcessorListener {
|
||||
private static Logger log = Logger.getLogger(MetadataManager.class);
|
||||
private final static int maximumMetadataLength = 4095;
|
||||
|
||||
private final static Pattern field = Pattern.compile("(\\$\\{([^\\}\\$]+)\\})|(\\$([^\\s\\?\\$]+))");
|
||||
private final static Pattern condition = Pattern.compile("\\?\\(([^,]+),([^\\)]+)\\)");
|
||||
|
||||
private String format = "$artist ?(album,- )$album ?(title,- )$title";
|
||||
private String cachedMetadataString = "";
|
||||
private Metadata currentMetadata = null;
|
||||
|
||||
private long sendMetadataInterval = 15000;
|
||||
private long lastMetadataChunk = 0;
|
||||
|
||||
/**
|
||||
* Default constructor. Sets the metadata to "Nothing playing"
|
||||
*/
|
||||
public MetadataManager() {
|
||||
setMetadata(Metadata.create("Nothing playing", null, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats and returns a byte array containing metadata information from the MediaQueue.
|
||||
* @param mediaQueue
|
||||
* @return
|
||||
*/
|
||||
public byte [] getMetaData(MediaQueue mediaQueue) {
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
if (time - lastMetadataChunk > sendMetadataInterval) {
|
||||
log.debug("Send full metadata chunk ("+cachedMetadataString+")");
|
||||
|
||||
//.. return a full metadata string
|
||||
lastMetadataChunk = time;
|
||||
|
||||
// restrict the length of the metadata
|
||||
if (cachedMetadataString.length() > maximumMetadataLength ) {
|
||||
cachedMetadataString = cachedMetadataString.substring(0, maximumMetadataLength);
|
||||
}
|
||||
|
||||
int metadataLenth = cachedMetadataString.length();
|
||||
int encodedLength = ((int)Math.ceil(metadataLenth / 16.0));
|
||||
int blockLength = 16 * encodedLength;
|
||||
|
||||
byte [] result = new byte[blockLength+1];
|
||||
result[0] = (byte)encodedLength;
|
||||
|
||||
System.arraycopy(cachedMetadataString.getBytes(), 0, result, 1, metadataLenth);
|
||||
|
||||
// add padding to the block
|
||||
for (int i=metadataLenth+1;i<blockLength;i++) {
|
||||
result[i] = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
log.debug("Sending zero length metadata chunk");
|
||||
// return a zero length metadata chunk
|
||||
return new byte[] {0};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders the metadata string sent to the client.
|
||||
* @param format
|
||||
* @param info
|
||||
* @return
|
||||
*/
|
||||
protected String parseFormat(String format, Metadata info) {
|
||||
String result = format;
|
||||
|
||||
//.. replace all fields in the format string
|
||||
Matcher fieldmatch = field.matcher(format);
|
||||
while (fieldmatch.find()) {
|
||||
String fieldname = fieldmatch.group(2);
|
||||
|
||||
if (fieldname == null) {
|
||||
fieldname = fieldmatch.group(4);
|
||||
}
|
||||
|
||||
if (fieldname == null) {
|
||||
log.warn("Metadata format uses invalid field syntax '"+fieldmatch.group(0)+"'");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
MetadataType type = MetadataType.valueOf(fieldname.toUpperCase());
|
||||
|
||||
// if there is metadata for this field, insert the data into the string
|
||||
// otherwise just remove the expression from the string
|
||||
if ( StringUtils.hasText( info.get(type) ) ) {
|
||||
result = result.replace(fieldmatch.group(0), info.get(type));
|
||||
} else {
|
||||
result = result.replace(fieldmatch.group(0), "");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Metadata format uses invalid field name '"+fieldname+"' in expression '"+fieldmatch.group(0)+"'");
|
||||
}
|
||||
}
|
||||
|
||||
//.. replace all conditionals in the format string
|
||||
Matcher conditionmatch = condition.matcher(format);
|
||||
while (conditionmatch.find()) {
|
||||
String fieldname = conditionmatch.group(1);
|
||||
String text = conditionmatch.group(2);
|
||||
|
||||
if (fieldname == null) {
|
||||
log.warn("Metadata format uses invalid field syntax '"+fieldmatch.group(0)+"'");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
MetadataType type = MetadataType.valueOf(fieldname.toUpperCase());
|
||||
if ( StringUtils.hasText( info.get(type) ) ) {
|
||||
result = result.replace(conditionmatch.group(0), text);
|
||||
} else {
|
||||
result = result.replace(conditionmatch.group(0), "");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Metadata format uses invalid field name '"+fieldname+"' in expression '"+conditionmatch.group(0)+"'");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
|
||||
public void setFormat(String format) {
|
||||
this.format = format;
|
||||
|
||||
// call setMetadata to rerender the metadata string
|
||||
setMetadata(currentMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void nextMedia(Media media, Metadata metadata) {
|
||||
log.debug("nextMedia event triggered");
|
||||
setMetadata(metadata);
|
||||
|
||||
// trigger a full metadata chunk
|
||||
lastMetadataChunk = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and renders new metadata with the current format.
|
||||
* @param metadata
|
||||
*/
|
||||
public void setMetadata(Metadata metadata) {
|
||||
log.debug("New metadata set ["+metadata+"] Thread: "+Thread.currentThread());
|
||||
currentMetadata = metadata;
|
||||
cachedMetadataString = parseFormat("StreamTitle='"+getFormat(), currentMetadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
public class TimeOffsetBuffer {
|
||||
private int [] offset;
|
||||
private long [] time;
|
||||
private int bufferSize;
|
||||
private int readIndex;
|
||||
private int writeIndex;
|
||||
|
||||
public TimeOffsetBuffer(int bufferSize) {
|
||||
this.bufferSize = bufferSize;
|
||||
this.offset = new int[bufferSize];
|
||||
this.time = new long[bufferSize];
|
||||
|
||||
readIndex = 0;
|
||||
writeIndex = 0;
|
||||
}
|
||||
|
||||
public synchronized void write(int offset, int time) {
|
||||
this.offset[writeIndex] = offset;
|
||||
this.time[writeIndex] = time;
|
||||
|
||||
increaseWriteIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the closest offset for the given time.
|
||||
* @param time
|
||||
* @return -1 if the time is larger than any time in the buffer
|
||||
*/
|
||||
public synchronized int getOffset(long time) {
|
||||
for (int i=1;i<getBufferContentSize();i++) {
|
||||
int index = (readIndex + i) % bufferSize;
|
||||
|
||||
if (this.time[index] > time) {
|
||||
if (index > 0) {
|
||||
return this.offset[index-1];
|
||||
} else {
|
||||
return this.offset[bufferSize-1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public long getMinTime() {
|
||||
return this.time[readIndex];
|
||||
}
|
||||
|
||||
public long getMaxTime() {
|
||||
if (writeIndex > 0) {
|
||||
return this.offset[writeIndex-1];
|
||||
} else {
|
||||
return this.offset[bufferSize-1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in the buffer.
|
||||
* @return
|
||||
*/
|
||||
private int getBufferContentSize() {
|
||||
if (readIndex <= writeIndex) {
|
||||
return writeIndex-readIndex;
|
||||
}
|
||||
|
||||
return bufferSize-readIndex + writeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the write index by one. This method also adjusts the
|
||||
* readIndex.
|
||||
*/
|
||||
private void increaseWriteIndex() {
|
||||
writeIndex++;
|
||||
|
||||
if (writeIndex == readIndex) {
|
||||
readIndex++;
|
||||
}
|
||||
|
||||
if (writeIndex >= bufferSize) {
|
||||
writeIndex -= bufferSize;
|
||||
}
|
||||
|
||||
if (readIndex >= bufferSize) {
|
||||
readIndex -= bufferSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
public class CantCreateMediaReaderException extends RuntimeException {
|
||||
|
||||
public CantCreateMediaReaderException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public CantCreateMediaReaderException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public CantCreateMediaReaderException(String arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public CantCreateMediaReaderException(Throwable arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
/**
|
||||
* Exception class for duplicate item errors in search tree insertions.
|
||||
*
|
||||
* @author Mark Allen Weiss
|
||||
*/
|
||||
public class DuplicateItemException extends RuntimeException {
|
||||
/**
|
||||
* Construct this exception object.
|
||||
*/
|
||||
public DuplicateItemException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct this exception object.
|
||||
*
|
||||
* @param message
|
||||
* the error message.
|
||||
*/
|
||||
public DuplicateItemException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
/**
|
||||
* Encapsules an exception in a RuntimeException.
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class EncapsuledExceptionRuntimException extends RuntimeException {
|
||||
private static final long serialVersionUID = 794221656425404393L;
|
||||
|
||||
private Exception exception = null;
|
||||
|
||||
public EncapsuledExceptionRuntimException(Exception exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public void rethrow() throws Exception {
|
||||
throw this.exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
public class FrameHasNotBeenLoadedException extends RuntimeException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 8666136563915734945L;
|
||||
|
||||
public FrameHasNotBeenLoadedException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameHasNotBeenLoadedException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameHasNotBeenLoadedException(String arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameHasNotBeenLoadedException(Throwable arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
public class FrameIsTooOldException extends RuntimeException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -3486181476553301458L;
|
||||
|
||||
public FrameIsTooOldException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameIsTooOldException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameIsTooOldException(String message) {
|
||||
super(message);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameIsTooOldException(Throwable cause) {
|
||||
super(cause);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
public class FrameStorageIsEmptyException extends RuntimeException {
|
||||
private static final long serialVersionUID = -5393933470236337451L;
|
||||
|
||||
public FrameStorageIsEmptyException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameStorageIsEmptyException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameStorageIsEmptyException(String arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public FrameStorageIsEmptyException(Throwable arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
public class IllegalRequestException extends Exception {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -8418140559118657718L;
|
||||
|
||||
public IllegalRequestException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public IllegalRequestException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public IllegalRequestException(String arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public IllegalRequestException(Throwable arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package se.slackers.locality.exception;
|
||||
|
||||
public class InvalidBufferPositionException extends RuntimeException {
|
||||
|
||||
public InvalidBufferPositionException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidBufferPositionException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidBufferPositionException(String arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidBufferPositionException(Throwable arg0) {
|
||||
super(arg0);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
88
src/main/java/se/slackers/locality/media/Frame.java
Normal file
88
src/main/java/se/slackers/locality/media/Frame.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package se.slackers.locality.media;
|
||||
|
||||
public class Frame {
|
||||
/**
|
||||
* Frame length in ms
|
||||
*/
|
||||
private long length;
|
||||
|
||||
/**
|
||||
* Frame size in bytes
|
||||
*/
|
||||
private int size;
|
||||
|
||||
/**
|
||||
* Data buffer.
|
||||
*/
|
||||
private byte [] data;
|
||||
|
||||
|
||||
public Frame() {
|
||||
length = 0;
|
||||
size = 0;
|
||||
data = null;
|
||||
}
|
||||
|
||||
public Frame(int allocationSize) {
|
||||
length = 0;
|
||||
size = 0;
|
||||
data = new byte[allocationSize];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a deep copy of the given frame.
|
||||
* @param frame
|
||||
*/
|
||||
public Frame(Frame frame) {
|
||||
length = frame.length;
|
||||
size = frame.size;
|
||||
data = new byte[size];
|
||||
System.arraycopy(frame.data, 0, data, 0, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of this frame in milliseconds
|
||||
* @return Length of frame in Ms
|
||||
*/
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(long length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the frame in bytes.
|
||||
* @return Size of frame in bytes
|
||||
*/
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the data
|
||||
* @return
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append(getLength());
|
||||
str.append(" ms [");
|
||||
str.append(getSize());
|
||||
str.append(" bytes]");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package se.slackers.locality.media.queue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.exception.EncapsuledExceptionRuntimException;
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.media.reader.MediaReader;
|
||||
import se.slackers.locality.media.reader.MediaReaderFactory;
|
||||
import se.slackers.locality.media.reader.SilentMediaReader;
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
|
||||
public abstract class AbstractMediaQueueProcessor implements MediaQueueProcessor, Runnable {
|
||||
private static final Logger log = Logger.getLogger(AbstractMediaQueueProcessor.class);
|
||||
|
||||
private MediaQueue mediaQueue;
|
||||
private MediaReader mediaReader = null;
|
||||
private MediaReader silentReader = new SilentMediaReader();
|
||||
private MediaReaderFactory mediaReaderFactory = null;
|
||||
|
||||
private List<WeakReference<MediaQueueProcessorListener>> mediaQueueProcessorListeners = new ArrayList<WeakReference<MediaQueueProcessorListener>>();
|
||||
|
||||
private boolean stopProcessing;
|
||||
private int activeClients = 0;
|
||||
|
||||
private Object stopProcessingMonitor = new Object();
|
||||
private Semaphore initDeinit = new Semaphore(1);
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized void init() {
|
||||
log.info("Acquire permit");
|
||||
initDeinit.acquireUninterruptibly();
|
||||
|
||||
activeClients = 0;
|
||||
stopProcessing = false;
|
||||
|
||||
checkMediaReader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public synchronized void deinit() {
|
||||
mediaQueue.getFrameStorage().clear();
|
||||
|
||||
if (mediaReader != null) {
|
||||
try {
|
||||
mediaReader.close();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mediaReader = null;
|
||||
log.info("Releasing permit");
|
||||
initDeinit.release();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void run() {
|
||||
Frame frame = new Frame(3000); // Maximum frame size for an mp3 is 2881
|
||||
try {
|
||||
while (stopProcessing == false) {
|
||||
// If nothing is playing and there is nothing in the queue, exit the thread
|
||||
if (mediaReader == null && mediaQueue.size() == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
checkMediaReader();
|
||||
|
||||
// .. read a frame and store it in the buffer
|
||||
if (mediaReader != null) {
|
||||
synchronized (this) {
|
||||
readData(mediaReader, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Fix the error handling
|
||||
} catch (RuntimeException e) {
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Notify all waiting threads
|
||||
if (stopProcessing) {
|
||||
synchronized(stopProcessingMonitor) {
|
||||
stopProcessingMonitor.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that there is a valid {@link MediaReader} instantiated as long as there are more entries in the queue.
|
||||
*/
|
||||
private void checkMediaReader() {
|
||||
if (mediaReader == null && mediaQueue.size() > 0) {
|
||||
// create a new mediaReader for the next media file in queue.
|
||||
mediaReader = mediaReaderFactory.getMediaReader(mediaQueue.get(0));
|
||||
try {
|
||||
mediaReader.open(mediaQueue.get(0));
|
||||
fireNextMediaEvent(mediaQueue.get(0), mediaReader.getMetadata());
|
||||
} catch (IOException e) {
|
||||
log.error("Can't open [" + mediaQueue.get(0) + "], skipping file");
|
||||
mediaQueue.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaReader != null && mediaReader.eof()) {
|
||||
try {
|
||||
mediaReader.close();
|
||||
mediaReader = null;
|
||||
|
||||
// consume the played file
|
||||
mediaQueue.remove(0);
|
||||
|
||||
if (mediaQueue.size() > 0) {
|
||||
// create a new mediaReader for the next media file in
|
||||
// queue.
|
||||
mediaReader = mediaReaderFactory.getMediaReader(mediaQueue.get(0));
|
||||
mediaReader.open(mediaQueue.get(0));
|
||||
fireNextMediaEvent(mediaQueue.get(0), mediaReader.getMetadata());
|
||||
} else {
|
||||
mediaReader = silentReader;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Can't open [" + mediaQueue.get(0) + "], skipping file");
|
||||
mediaQueue.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a frame from the media and stores it in the FrameStorage.
|
||||
*
|
||||
* @param reader
|
||||
* @param frame
|
||||
* @return
|
||||
*/
|
||||
abstract protected void readData(MediaReader reader, Frame frame);
|
||||
|
||||
/**
|
||||
* Stops the processing and waits until it really has stopped.
|
||||
*/
|
||||
public void stopProcessing() {
|
||||
this.stopProcessing = true;
|
||||
|
||||
try {
|
||||
synchronized(stopProcessingMonitor) {
|
||||
stopProcessingMonitor.wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new EncapsuledExceptionRuntimException(e);
|
||||
}
|
||||
|
||||
deinit();
|
||||
|
||||
mediaQueue.stopProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mediaQueue that is used by the processor
|
||||
*/
|
||||
public MediaQueue getMediaQueue() {
|
||||
return mediaQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media queue to be used by the processor. This method also ensures that the reverse dependency is
|
||||
* correct.
|
||||
*/
|
||||
public void setMediaQueue(MediaQueue mediaQueue) {
|
||||
this.mediaQueue = mediaQueue;
|
||||
if (this.mediaQueue.getMediaQueueProcessor() != this) {
|
||||
this.mediaQueue.setMediaQueueProcessor(this);
|
||||
}
|
||||
}
|
||||
|
||||
public MediaReaderFactory getMediaReaderFactory() {
|
||||
return mediaReaderFactory;
|
||||
}
|
||||
|
||||
public void setMediaReaderFactory(MediaReaderFactory mediaReaderFactory) {
|
||||
this.mediaReaderFactory = mediaReaderFactory;
|
||||
}
|
||||
|
||||
public void clientStartStreaming() {
|
||||
activeClients++;
|
||||
log.debug("Client connected, client="+activeClients);
|
||||
|
||||
// Update metadata for the connected client
|
||||
/*
|
||||
if (mediaQueue != null && mediaReader != null) {
|
||||
fireNextMediaEvent(mediaQueue.get(0), mediaReader.getMetadata());
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public void clientStopsStreaming() {
|
||||
activeClients--;
|
||||
log.debug("Client disconnected, client="+activeClients);
|
||||
|
||||
if (activeClients == 0) {
|
||||
// No clients listening, stop the stream
|
||||
stopProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
public void addMediaQueueProcessorListener(MediaQueueProcessorListener listener) {
|
||||
log.debug("New listener added by thread "+Thread.currentThread());
|
||||
mediaQueueProcessorListeners.add(new WeakReference<MediaQueueProcessorListener>(listener));
|
||||
}
|
||||
|
||||
public void removeMediaQueueProcessorListener(MediaQueueProcessorListener listener) {
|
||||
mediaQueueProcessorListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the nextMedia method in all registered MediaQueueListeners
|
||||
* @param media
|
||||
*/
|
||||
protected void fireNextMediaEvent(Media media, Metadata metadata) {
|
||||
Iterator<WeakReference<MediaQueueProcessorListener>> iterator = mediaQueueProcessorListeners.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
WeakReference<MediaQueueProcessorListener> ref = iterator.next();
|
||||
|
||||
if (null == ref.get()) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
ref.get().nextMedia(media, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Metadata getCurrentMetadata() {
|
||||
if (mediaReader == null) {
|
||||
return Metadata.create("Nothing playing", null, null);
|
||||
}
|
||||
return mediaReader.getMetadata();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package se.slackers.locality.media.queue;
|
||||
|
||||
import se.slackers.locality.data.FrameStorage;
|
||||
import se.slackers.locality.model.Media;
|
||||
|
||||
public interface MediaQueue {
|
||||
|
||||
/**
|
||||
* Returns the name of the MediaQueue
|
||||
* @return The name of the MediaQueue
|
||||
*/
|
||||
public String getName();
|
||||
|
||||
public void add(Media media);
|
||||
public Media get(int index);
|
||||
public void remove(int index);
|
||||
public int size();
|
||||
|
||||
public void setMountPoint(String mountPoint);
|
||||
public String getMountPoint();
|
||||
|
||||
public void startProcessor();
|
||||
public void stopProcessor();
|
||||
public boolean isProcessorRunning();
|
||||
|
||||
public MediaQueueProcessor getMediaQueueProcessor();
|
||||
public void setMediaQueueProcessor(MediaQueueProcessor queueProcessor);
|
||||
|
||||
public FrameStorage getFrameStorage();
|
||||
public void setFrameStorage(FrameStorage frameStorage);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package se.slackers.locality.media.queue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.data.FrameStorage;
|
||||
import se.slackers.locality.model.Media;
|
||||
|
||||
public class MediaQueueImpl implements MediaQueue {
|
||||
private static final Logger log = Logger.getLogger(MediaQueueImpl.class);
|
||||
|
||||
private String mountPoint;
|
||||
private String queueName;
|
||||
|
||||
private List<Media> queue;
|
||||
|
||||
private Semaphore startStop = new Semaphore(1);
|
||||
|
||||
private MediaQueueProcessor processor = null;
|
||||
private FrameStorage frameStorage = null;
|
||||
private Thread processorThread = null;
|
||||
|
||||
public MediaQueueImpl(String queueName) {
|
||||
this.queueName = queueName;
|
||||
queue = Collections.synchronizedList(new LinkedList<Media>());
|
||||
}
|
||||
|
||||
public MediaQueueProcessor getMediaQueueProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the MediaQueueProcessor to be used by the media queue. This method
|
||||
* ensures that the reverse dependency is correct.
|
||||
*/
|
||||
public void setMediaQueueProcessor(MediaQueueProcessor processor) {
|
||||
this.processor = processor;
|
||||
|
||||
if (this.processor.getMediaQueue() != this) {
|
||||
this.processor.setMediaQueue(this);
|
||||
}
|
||||
}
|
||||
|
||||
public FrameStorage getFrameStorage() {
|
||||
return frameStorage;
|
||||
}
|
||||
|
||||
public void setFrameStorage(FrameStorage frameStorage) {
|
||||
this.frameStorage = frameStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media to the queue.
|
||||
* @param media
|
||||
*/
|
||||
public void add(Media media) {
|
||||
queue.add(media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns media from a certain position in the queue.
|
||||
*/
|
||||
public Media get(int index) {
|
||||
return queue.get(index);
|
||||
}
|
||||
|
||||
public void remove(int index) {
|
||||
if (!queue.isEmpty()) {
|
||||
queue.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
public String getMountPoint() {
|
||||
return mountPoint;
|
||||
}
|
||||
|
||||
public void setMountPoint(String mountPoint) {
|
||||
this.mountPoint = mountPoint;
|
||||
}
|
||||
|
||||
public synchronized boolean isProcessorRunning() {
|
||||
if (processorThread == null)
|
||||
return false;
|
||||
|
||||
return processorThread.isAlive();
|
||||
}
|
||||
|
||||
public synchronized void startProcessor() {
|
||||
log.info("Waiting to start processor ["+(processorThread == null || processorThread.isAlive() == false)+"]");
|
||||
startStop.acquireUninterruptibly();
|
||||
log.info("Starting processor ["+(processorThread == null || processorThread.isAlive() == false)+"]");
|
||||
|
||||
if (processorThread == null || processorThread.isAlive() == false) {
|
||||
processor.init();
|
||||
processorThread = new Thread(processor, "QueueProcessor["+getMountPoint()+"]");
|
||||
processorThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void stopProcessor() {
|
||||
/*
|
||||
if (isProcessorRunning()) {
|
||||
processor.stopProcessing();
|
||||
processor.deinit();
|
||||
processorThread = null;
|
||||
}
|
||||
*/
|
||||
log.info("Releasing startStop lock");
|
||||
startStop.release();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return queueName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package se.slackers.locality.media.queue;
|
||||
|
||||
import se.slackers.locality.media.reader.MediaReaderFactory;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
import se.slackers.locality.shout.ClientListener;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public interface MediaQueueProcessor extends Runnable, ClientListener {
|
||||
public void init();
|
||||
public void deinit();
|
||||
|
||||
/**
|
||||
* Stop the processor and wait until it really is stopped.
|
||||
*/
|
||||
public void stopProcessing();
|
||||
|
||||
public void setMediaQueue(MediaQueue mediaQueue);
|
||||
public MediaQueue getMediaQueue();
|
||||
|
||||
public void setMediaReaderFactory(MediaReaderFactory mediaReaderFactory);
|
||||
public MediaReaderFactory getMediaReaderFactory();
|
||||
|
||||
public void addMediaQueueProcessorListener(MediaQueueProcessorListener listener);
|
||||
public void removeMediaQueueProcessorListener(MediaQueueProcessorListener listener);
|
||||
|
||||
public Metadata getCurrentMetadata();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package se.slackers.locality.media.queue;
|
||||
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
|
||||
public interface MediaQueueProcessorListener {
|
||||
|
||||
/**
|
||||
* Called when the MediaQueue advances to the next Media in queue.
|
||||
* @param media The media that will be played.
|
||||
* @param metadata The metadata for the media
|
||||
*/
|
||||
public void nextMedia(Media media, Metadata metadata);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package se.slackers.locality.media.queue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.data.FrameStorage;
|
||||
import se.slackers.locality.data.FrameStorageEntry;
|
||||
import se.slackers.locality.exception.EncapsuledExceptionRuntimException;
|
||||
import se.slackers.locality.exception.FrameStorageIsEmptyException;
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.media.reader.MediaReader;
|
||||
|
||||
public class PreloadDataMediaQueueProcessor extends AbstractMediaQueueProcessor {
|
||||
private static final Logger log = Logger.getLogger(PreloadDataMediaQueueProcessor.class);
|
||||
|
||||
private long maximumPreload = 5000;
|
||||
private long maximumHistory = 5000;
|
||||
|
||||
private void sleep(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
throw new EncapsuledExceptionRuntimException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected void readData(MediaReader reader, Frame frame) {
|
||||
FrameStorage storage = getMediaQueue().getFrameStorage();
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long lastFrameTime = currentTime;
|
||||
|
||||
// first purge the history
|
||||
storage.purgeUntil(currentTime - maximumHistory);
|
||||
|
||||
try {
|
||||
lastFrameTime = storage.getLastFrameTime();
|
||||
|
||||
// Make sure we don't use old time stamps when we store frames
|
||||
if (lastFrameTime < currentTime) {
|
||||
lastFrameTime = currentTime;
|
||||
log.warn("FrameReader is lagging");
|
||||
}
|
||||
|
||||
} catch (FrameStorageIsEmptyException e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (lastFrameTime > currentTime + maximumPreload) {
|
||||
// the maximum data preload have been reached, stall the thread for a while.
|
||||
sleep(250);
|
||||
return;
|
||||
}
|
||||
|
||||
//.. read more data from the media
|
||||
try {
|
||||
reader.readFrame(frame);
|
||||
storage.add( new FrameStorageEntry(lastFrameTime, new Frame(frame) ) );
|
||||
} catch (IOException e) {
|
||||
throw new EncapsuledExceptionRuntimException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package se.slackers.locality.media.reader;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
||||
|
||||
public class ByteStreamReader {
|
||||
private static final Logger log = Logger.getLogger(ByteStreamReader.class);
|
||||
|
||||
private InputStream inputStream = null;
|
||||
private ByteBuffer buffer = null;
|
||||
private byte[] tempbuffer = null;
|
||||
|
||||
private int readIndex = 0;
|
||||
private int writeIndex = 0;
|
||||
private int indexOffset = 0;
|
||||
|
||||
public ByteStreamReader() {
|
||||
// buffer = ByteBuffer.allocateDirect(1024*1024);
|
||||
// tempbuffer = new byte[65536];
|
||||
buffer = ByteBuffer.allocateDirect(1024*1024);
|
||||
tempbuffer = new byte[65536];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the read-cursor
|
||||
* @return
|
||||
*/
|
||||
public int getOffset() {
|
||||
return indexOffset + readIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one byte from the current position.
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public byte read() throws IOException {
|
||||
if (readIndex >= writeIndex) {
|
||||
if (0 == readData()) {
|
||||
throw new EOFException();
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.get(readIndex++);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param destbuffer
|
||||
* @param offset
|
||||
* @param length
|
||||
* @throws IOException
|
||||
*/
|
||||
public void read(byte [] destbuffer, int offset, int length) throws IOException {
|
||||
assert length < tempbuffer.length : "The requested data is bigger than the tempbuffer";
|
||||
|
||||
if (readIndex + length >= writeIndex) {
|
||||
if (0 == readData()) {
|
||||
throw new EOFException();
|
||||
}
|
||||
}
|
||||
|
||||
buffer.position(readIndex);
|
||||
buffer.get(destbuffer, offset, length);
|
||||
readIndex += length;
|
||||
}
|
||||
|
||||
private int readData() throws IOException {
|
||||
if (buffer.limit() - writeIndex < tempbuffer.length) {
|
||||
recycleBuffer();
|
||||
}
|
||||
|
||||
int bytes = inputStream.read(tempbuffer, 0, tempbuffer.length);
|
||||
|
||||
buffer.position(writeIndex);
|
||||
buffer.put(tempbuffer, 0, bytes);
|
||||
writeIndex += bytes;
|
||||
|
||||
log.info("[" + bytes + " bytes read]");
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycle the buffer
|
||||
*/
|
||||
private void recycleBuffer() {
|
||||
if (readIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Recycling ByteBuffer");
|
||||
|
||||
int indexAdjustment = readIndex;
|
||||
int recycleIndex = 0;
|
||||
|
||||
// the data that is to be recycled is larger than tempbuffer
|
||||
while (writeIndex - readIndex > tempbuffer.length) {
|
||||
buffer.position(readIndex);
|
||||
buffer.get(tempbuffer, 0, tempbuffer.length);
|
||||
buffer.position(recycleIndex);
|
||||
buffer.put(tempbuffer, 0, tempbuffer.length);
|
||||
|
||||
recycleIndex += tempbuffer.length;
|
||||
readIndex += tempbuffer.length;
|
||||
}
|
||||
|
||||
if (writeIndex - readIndex > 0) {
|
||||
buffer.position(readIndex);
|
||||
buffer.get(tempbuffer, 0, writeIndex - readIndex);
|
||||
buffer.position(recycleIndex);
|
||||
buffer.put(tempbuffer, 0, writeIndex - readIndex);
|
||||
}
|
||||
|
||||
indexOffset += indexAdjustment;
|
||||
writeIndex -= indexAdjustment;
|
||||
readIndex = 0;
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public void setInputStream(InputStream inputStream) {
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package se.slackers.locality.media.reader;
|
||||
|
||||
public class ByteUtils {
|
||||
/**
|
||||
* Converts a byte to a string of ones and zeroes.
|
||||
* @param b
|
||||
* @return
|
||||
*/
|
||||
public static String byte2String(byte b) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
int value = 0x80;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if ((b & value) == 0) {
|
||||
sb.append("0");
|
||||
} else {
|
||||
sb.append("1");
|
||||
}
|
||||
value >>= 1;
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package se.slackers.locality.media.reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
|
||||
public interface MediaReader {
|
||||
/**
|
||||
* Returns true if this reader supports the format of the given media.
|
||||
* @param media
|
||||
* @return
|
||||
*/
|
||||
public boolean supports(Media media);
|
||||
|
||||
/**
|
||||
* Opens the media file.
|
||||
* @param media
|
||||
* @throws IOException
|
||||
*/
|
||||
public void open(Media media) throws IOException ;
|
||||
|
||||
/**
|
||||
* Reads one frame of the media.
|
||||
* @param frame
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public Frame readFrame(Frame frame) throws IOException;
|
||||
|
||||
/**
|
||||
* Closes the media.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void close() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns true if the end of the media is reached.
|
||||
* @return
|
||||
*/
|
||||
public boolean eof();
|
||||
|
||||
/**
|
||||
* Returns information about the media, artist, title. This function can be
|
||||
* called multiple times but it only needs to have valid metadata after open has
|
||||
* been called. It must never return null.
|
||||
* @return
|
||||
*/
|
||||
public Metadata getMetadata();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package se.slackers.locality.media.reader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import se.slackers.locality.exception.CantCreateMediaReaderException;
|
||||
import se.slackers.locality.model.Media;
|
||||
|
||||
public class MediaReaderFactory {
|
||||
private List<MediaReader> mediaReaders = new ArrayList<MediaReader>();
|
||||
|
||||
public void addMediaReader(MediaReader mediaReader) {
|
||||
mediaReaders.add(mediaReader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MediaReader that can handle the given media file.
|
||||
* @throws CantCreateMediaReaderException
|
||||
* @param media
|
||||
* @return
|
||||
*/
|
||||
public MediaReader getMediaReader(Media media) {
|
||||
for (MediaReader reader : mediaReaders) {
|
||||
if (reader.supports(media)) {
|
||||
try {
|
||||
return (MediaReader) reader.getClass().newInstance();
|
||||
} catch (InstantiationException e) {
|
||||
throw new CantCreateMediaReaderException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new CantCreateMediaReaderException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new CantCreateMediaReaderException("No supported reader found for the media file ["+media.getMediaFile()+"]");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package se.slackers.locality.media.reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
|
||||
public class SilentMediaReader implements MediaReader {
|
||||
|
||||
byte [] emptyFrame = new byte[] {(byte) 0xff, (byte) 0xf2, 0x10, (byte) 0xc4, 0x1b, 0x27, 0x0, 0x0, 0x0, 0x3, (byte) 0xfc, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x41, 0x4d, 0x45, 0x33, 0x2e, 0x39, 0x37, 0x0, 0x0, 0x0, (byte) 0xff, (byte) 0xf2, 0x10, (byte) 0xc4, 0x1b, 0x27, 0x0, 0x0, 0x0, 0x3, (byte) 0xfc, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x41, 0x4d, 0x45, 0x33, 0x2e, 0x39, 0x37, 0x0, 0x0, 0x0};
|
||||
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
public boolean eof() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void open(Media media) throws IOException {
|
||||
}
|
||||
|
||||
public Frame readFrame(Frame frame) throws IOException {
|
||||
|
||||
frame.setLength(26);
|
||||
frame.setSize(emptyFrame.length);
|
||||
System.arraycopy(emptyFrame, 0, frame.getData(), 0, emptyFrame.length);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method always returns false so the reader isn't used by mistake.
|
||||
*/
|
||||
public boolean supports(Media media) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Metadata getMetadata() {
|
||||
return Metadata.create("", "", "No media playing");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package se.slackers.locality.media.reader.mp3;
|
||||
|
||||
|
||||
public class Mp3FrameHeader {
|
||||
//private static final Logger log = Logger.getLogger(Mp3FrameHeader.class);
|
||||
|
||||
private byte header[] = new byte[4];
|
||||
private long offset;
|
||||
|
||||
// DUMMY, MPEG1, MPEG2, MPEG2.5
|
||||
private static final int sampleRateTable[][] = {
|
||||
{ 0, 44100, 22050, 11025 }, { 0, 48000, 24000, 12000 },
|
||||
{ 0, 32000, 16000, 8000 }, { 0, 0, 0, 0 } };
|
||||
|
||||
// V1 L1, V1 L2, V1 L3, V2 L1, V2 L2 & L3
|
||||
private static final int bitRateTable[][] = { { 0, 0, 0, 0, 0 },
|
||||
{ 32, 32, 32, 32, 8 }, { 64, 48, 40, 48, 16 },
|
||||
{ 96, 56, 48, 56, 24 }, { 128, 64, 56, 64, 32 },
|
||||
{ 160, 80, 64, 80, 40 }, { 192, 96, 80, 96, 48 },
|
||||
{ 224, 112, 96, 112, 56 }, { 256, 128, 112, 128, 64 },
|
||||
{ 288, 160, 128, 144, 80 }, { 320, 192, 160, 160, 96 },
|
||||
{ 352, 224, 192, 176, 112 }, { 384, 256, 224, 192, 128 },
|
||||
{ 416, 320, 256, 224, 144 }, { 448, 384, 320, 256, 160 },
|
||||
{ 0, 0, 0, 0, 0 } };
|
||||
|
||||
public Mp3FrameHeader() {
|
||||
}
|
||||
|
||||
public void setData(byte [] data) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
header[i] = data[i];
|
||||
}
|
||||
}
|
||||
|
||||
public void setData(byte b1, byte b2, byte b3, byte b4) {
|
||||
header[0] = b1;
|
||||
header[1] = b2;
|
||||
header[2] = b3;
|
||||
header[3] = b4;
|
||||
}
|
||||
|
||||
public byte [] getData() {
|
||||
return header;
|
||||
}
|
||||
|
||||
/*
|
||||
* 7 6 5 4 3 2 1 0 128 64 32 16 8 4 2 1 0x80 0x40 0x20 0x10 0x08 0x04 0x02
|
||||
* 0x01
|
||||
*
|
||||
* A = 10 B = 11 C = 12 D = 13 E = 14 F = 15
|
||||
*/
|
||||
|
||||
public int getMPEGVersion() {
|
||||
return 4 - ((header[1] & 0x18) >> 3);
|
||||
}
|
||||
|
||||
public int getLayerDescription() {
|
||||
return 4 - ((header[1] & 0x06) >> 1);
|
||||
}
|
||||
|
||||
public boolean isCRCProtected() {
|
||||
return (header[1] & 0x01) == 1;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
int index = (header[2] & 0xf0) >> 4;
|
||||
|
||||
int v = getMPEGVersion();
|
||||
int l = getLayerDescription();
|
||||
|
||||
int index2 = Math.min((v - 1) * 3 + (l - 1), 4);
|
||||
|
||||
return bitRateTable[index][index2] * 1000;
|
||||
}
|
||||
|
||||
public boolean isPadded() {
|
||||
return (header[2] & 0x2) == 2;
|
||||
}
|
||||
|
||||
public int getSampleRate() {
|
||||
int index = (header[2] & 0x0e) >> 2;
|
||||
if (index < 0 || index > 3) {
|
||||
return 0;
|
||||
}
|
||||
int version = getMPEGVersion();
|
||||
return sampleRateTable[index][version];
|
||||
}
|
||||
|
||||
public int getChannelMode() {
|
||||
return ((header[3] & 0xC0) >> 6);
|
||||
}
|
||||
|
||||
public int getModeExtension() {
|
||||
return ((header[3] & 0x30) >> 4);
|
||||
}
|
||||
|
||||
public boolean isCopyrighted() {
|
||||
return (header[3] & 0x08) != 0;
|
||||
}
|
||||
|
||||
public boolean isOriginal() {
|
||||
return (header[3] & 0x04) != 0;
|
||||
}
|
||||
|
||||
public int getEmphasis() {
|
||||
return (header[3] & 0x03);
|
||||
}
|
||||
|
||||
public int getFrameSize() {
|
||||
int bitrate = getBitRate();
|
||||
int samplerate = getSampleRate();
|
||||
int padding = isPadded() ? 1 : 0;
|
||||
return 144 * bitrate / (samplerate + padding);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Mp3Header[" + offset + "] {" + "\n MPEG Version: "
|
||||
+ getMPEGVersion() + "\n Layer description: "
|
||||
+ getLayerDescription() + "\n Is CRC protected: "
|
||||
+ isCRCProtected() + "\n Bitrate: " + getBitRate()
|
||||
+ "\n Samplerate: " + getSampleRate() + "\n Is Padded: "
|
||||
+ isPadded() + "\n Channel mode: " + getChannelMode()
|
||||
+ "\n Mode Extension: " + getModeExtension() + "\n Copyright: "
|
||||
+ isCopyrighted() + "\n Is original: " + isOriginal()
|
||||
+ "\n Emphasis: " + getEmphasis() + "\n Frame size: "
|
||||
+ getFrameSize() + "\n}";
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public void setOffset(long offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package se.slackers.locality.media.reader.mp3;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.jaudiotagger.audio.AudioFile;
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.Tag;
|
||||
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.media.reader.ByteStreamReader;
|
||||
import se.slackers.locality.media.reader.MediaReader;
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.model.Metadata;
|
||||
import se.slackers.locality.model.MetadataType;
|
||||
|
||||
/*
|
||||
* 128kbps 44.1kHz layer II uses a lot of 418 bytes and some of 417 bytes long.
|
||||
* Regardless of the bitrate of the file, a frame in an MPEG-1 file lasts for 26ms (26/1000 of a second).
|
||||
*/
|
||||
public class Mp3MediaReader implements MediaReader {
|
||||
private static final Logger log = Logger.getLogger(Mp3MediaReader.class);
|
||||
|
||||
private ByteStreamReader reader = null;
|
||||
private Mp3FrameHeader header = new Mp3FrameHeader();
|
||||
private Metadata metadata = null;
|
||||
|
||||
public Mp3MediaReader() {
|
||||
super();
|
||||
|
||||
metadata = Metadata.create("Unknown", "", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public boolean supports(Media media) {
|
||||
return media.getMediaFile().toString().toLowerCase().endsWith(".mp3");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
if (reader != null) {
|
||||
reader.getInputStream().close();
|
||||
reader.setInputStream(null);
|
||||
reader = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void open(Media media) throws IOException {
|
||||
if (reader != null) {
|
||||
close();
|
||||
}
|
||||
|
||||
reader = new ByteStreamReader();
|
||||
reader.setInputStream(new FileInputStream(media.getMediaFile()));
|
||||
|
||||
// try to read some id3 tags from the media
|
||||
try {
|
||||
AudioFile audioFile = AudioFileIO.read(media.getMediaFile());
|
||||
Tag tag = audioFile.getTag();
|
||||
|
||||
metadata.set(MetadataType.ARTIST, tag.getFirstArtist() );
|
||||
metadata.set(MetadataType.ALBUM, tag.getFirstAlbum() );
|
||||
metadata.set(MetadataType.TITLE, tag.getFirstTitle() );
|
||||
|
||||
log.info("Opening "+metadata);
|
||||
} catch (Exception e) {
|
||||
log.error("Can't read tag from "+media);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public boolean eof() {
|
||||
try {
|
||||
return reader.getInputStream().available() <= 0;
|
||||
} catch (IOException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public Frame readFrame(Frame frame) throws IOException {
|
||||
Mp3FrameHeader header = findMp3Header();
|
||||
//log.info(header.toString());
|
||||
|
||||
frame.setSize(header.getFrameSize());
|
||||
frame.setLength(26);
|
||||
|
||||
System.arraycopy(header.getData(), 0, frame.getData(), 0, 4);
|
||||
reader.read(frame.getData(), 4, (int)frame.getSize()-4);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public Metadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Mp3FrameHeader findMp3Header() throws IOException {
|
||||
byte lastByte = 0;
|
||||
byte currentByte = 0;
|
||||
|
||||
while (true) {
|
||||
lastByte = currentByte;
|
||||
currentByte = reader.read();
|
||||
// Check for the start of the Mp3 Frame Header
|
||||
if (lastByte == (byte) 0xff && (currentByte & 0xE0) == 0xE0) {
|
||||
header.setOffset(reader.getOffset() - 2);
|
||||
header.setData(lastByte, currentByte, reader.read(), reader.read());
|
||||
|
||||
//log.info("Found frame start at index [" + (header.getOffset())
|
||||
// + "]");
|
||||
return header;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/main/java/se/slackers/locality/model/Media.java
Normal file
20
src/main/java/se/slackers/locality/model/Media.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package se.slackers.locality.model;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Media {
|
||||
private File mediaFile;
|
||||
|
||||
public File getMediaFile() {
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
public void setMediaFile(File mediaFile) {
|
||||
this.mediaFile = mediaFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Media: "+mediaFile.toString();
|
||||
}
|
||||
}
|
||||
122
src/main/java/se/slackers/locality/model/MetaTag.java
Executable file
122
src/main/java/se/slackers/locality/model/MetaTag.java
Executable file
@@ -0,0 +1,122 @@
|
||||
package se.slackers.locality.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.persistence.Basic;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinTable;
|
||||
import javax.persistence.ManyToMany;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author eb
|
||||
*
|
||||
*/
|
||||
@Entity
|
||||
public class MetaTag {
|
||||
private Long id;
|
||||
private String name;
|
||||
private List<Tag> tags;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public MetaTag() {
|
||||
id = new Long(0);
|
||||
name = "";
|
||||
tags = new ArrayList<Tag>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a metatag to the specified tag. This method fixes the the bidirectional dependency.
|
||||
* @see Tag#addMetaTag(MetaTag)
|
||||
* @param tag
|
||||
*/
|
||||
public void addTag(Tag tag) {
|
||||
assert tag != null : "The given tag is null";
|
||||
|
||||
getTags().add(tag);
|
||||
tag.getMetaTags().add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Id
|
||||
@Column(name = "id", unique = true)
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Basic
|
||||
@Column(name = "name", unique = true)
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@ManyToMany(targetEntity = Tag.class, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH })
|
||||
@JoinTable(name = "MetaTag_Tag", joinColumns = @JoinColumn(name = "MetaTag_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "Tag_id", referencedColumnName = "id"))
|
||||
public List<Tag> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
public void removeTag(Tag tag) {
|
||||
assert tag != null : "The given tag is null";
|
||||
|
||||
getTags().remove(tag);
|
||||
tag.getMetaTags().remove(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tags
|
||||
*/
|
||||
public void setTags(List<Tag> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public String toString() {
|
||||
int num = tags == null ? 0 : tags.size();
|
||||
return "MetaTag [id:"+id+", name:"+name+", tags: "+num+"]";
|
||||
}
|
||||
}
|
||||
52
src/main/java/se/slackers/locality/model/Metadata.java
Normal file
52
src/main/java/se/slackers/locality/model/Metadata.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package se.slackers.locality.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Holds some basic information about a media file.
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class Metadata {
|
||||
private Map<MetadataType, String> data = new HashMap<MetadataType, String>();
|
||||
|
||||
public static Metadata create(String artist, String album, String title) {
|
||||
Metadata info = new Metadata();
|
||||
|
||||
info.set(MetadataType.ARTIST, artist);
|
||||
info.set(MetadataType.ALBUM, album);
|
||||
info.set(MetadataType.TITLE, title);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void set(MetadataType type, String value) {
|
||||
data.put(type, value);
|
||||
}
|
||||
|
||||
public boolean has(MetadataType type) {
|
||||
return data.containsKey(type);
|
||||
}
|
||||
|
||||
public String get(MetadataType type) {
|
||||
return data.get(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuffer builder = new StringBuffer();
|
||||
|
||||
builder.append("[");
|
||||
for (MetadataType type : data.keySet()) {
|
||||
builder.append(data.get(type));
|
||||
builder.append(", ");
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.setLength(builder.length()-1);
|
||||
}
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package se.slackers.locality.model;
|
||||
|
||||
public enum MetadataType {
|
||||
ARTIST,
|
||||
ALBUM,
|
||||
TRACK,
|
||||
TITLE
|
||||
}
|
||||
122
src/main/java/se/slackers/locality/model/Tag.java
Executable file
122
src/main/java/se/slackers/locality/model/Tag.java
Executable file
@@ -0,0 +1,122 @@
|
||||
package se.slackers.locality.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.persistence.Basic;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToMany;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author eb
|
||||
*
|
||||
*/
|
||||
@Entity
|
||||
public class Tag implements Serializable {
|
||||
private static final long serialVersionUID = 825416798925249763L;
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private List<MetaTag> metaTags;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public Tag() {
|
||||
id = new Long(0);
|
||||
name = "";
|
||||
metaTags = new ArrayList<MetaTag>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a metatag to the tag. This method also fixes the the bidirectional dependency.
|
||||
* @see MetaTag#addTag(Tag)
|
||||
* @param metatag
|
||||
*/
|
||||
public void addMetaTag(MetaTag metatag) {
|
||||
assert metatag != null : "The given meta tag is null";
|
||||
|
||||
getMetaTags().add(metatag);
|
||||
metatag.getTags().add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Id
|
||||
@Column(name = "id", unique = true)
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@ManyToMany(targetEntity = MetaTag.class, cascade = { CascadeType.PERSIST, CascadeType.MERGE }, mappedBy = "tags")
|
||||
public List<MetaTag> getMetaTags() {
|
||||
return metaTags;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Basic
|
||||
@Column(name = "name", unique = true)
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param metatag
|
||||
*/
|
||||
public void removeMetaTag(MetaTag metatag) {
|
||||
assert metatag != null : "The given meta tag is null";
|
||||
|
||||
getMetaTags().remove(metatag);
|
||||
metatag.getTags().remove(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param metaTags
|
||||
*/
|
||||
public void setMetaTags(List<MetaTag> metaTags) {
|
||||
this.metaTags = metaTags;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public String toString() {
|
||||
int num = metaTags == null ? 0 : metaTags.size();
|
||||
return "Tag [id:" + id + ", name:" + name + ", metatags: " + num + "]";
|
||||
}
|
||||
}
|
||||
32
src/main/java/se/slackers/locality/net/HttpRequest.java
Normal file
32
src/main/java/se/slackers/locality/net/HttpRequest.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package se.slackers.locality.net;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import se.slackers.locality.exception.IllegalRequestException;
|
||||
|
||||
public class HttpRequest {
|
||||
private static final Pattern getRegexp = Pattern.compile("get\\s+([^\\s]+).*", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||
private String request;
|
||||
|
||||
public HttpRequest(String request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public String getRequestPath() throws IllegalRequestException {
|
||||
Matcher matcher = getRegexp.matcher(request);
|
||||
|
||||
if (matcher.matches()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
|
||||
throw new IllegalRequestException("Illegal request ["+request+"]");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public String toString() {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
14
src/main/java/se/slackers/locality/shout/ClientListener.java
Normal file
14
src/main/java/se/slackers/locality/shout/ClientListener.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
public interface ClientListener {
|
||||
/**
|
||||
* Called whenever a new ShoutRunnable is started to handle a client connection.
|
||||
*/
|
||||
public void clientStartStreaming();
|
||||
|
||||
/**
|
||||
* Called when a a ShoutRunnable is stopped.
|
||||
*/
|
||||
public void clientStopsStreaming();
|
||||
|
||||
}
|
||||
267
src/main/java/se/slackers/locality/shout/ShoutRunnable.java
Normal file
267
src/main/java/se/slackers/locality/shout/ShoutRunnable.java
Normal file
@@ -0,0 +1,267 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.data.FrameStorage;
|
||||
import se.slackers.locality.data.FrameStorageEntry;
|
||||
import se.slackers.locality.data.MetadataManager;
|
||||
import se.slackers.locality.exception.EncapsuledExceptionRuntimException;
|
||||
import se.slackers.locality.exception.FrameHasNotBeenLoadedException;
|
||||
import se.slackers.locality.exception.FrameIsTooOldException;
|
||||
import se.slackers.locality.exception.FrameStorageIsEmptyException;
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.media.queue.MediaQueue;
|
||||
import se.slackers.locality.net.HttpRequest;
|
||||
import se.slackers.locality.shout.manager.ShoutRequestManager;
|
||||
|
||||
public class ShoutRunnable implements Runnable {
|
||||
private static Logger log = Logger.getLogger(ShoutRunnable.class);
|
||||
private static final int metadataInterval = 65536;
|
||||
|
||||
private Socket socket;
|
||||
private ShoutRequestManager requestManager;
|
||||
private MetadataManager metadataManager;
|
||||
|
||||
protected boolean exit;
|
||||
private List<WeakReference<ClientListener>> clientConnectionListeners = new ArrayList<WeakReference<ClientListener>>();
|
||||
|
||||
public ShoutRunnable(ShoutRequestManager requestManager, Socket clientSocket) {
|
||||
this.socket = clientSocket;
|
||||
this.requestManager = requestManager;
|
||||
this.metadataManager = new MetadataManager();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
log.info(Thread.currentThread().getName() + " started");
|
||||
try {
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
|
||||
HttpRequest request = readRequest(in);
|
||||
log.info("Received request: "+request);
|
||||
|
||||
MediaQueue mediaQueue = null;
|
||||
try {
|
||||
mediaQueue = requestManager.processRequest(request);
|
||||
} catch (SecurityException e) {
|
||||
writeNegativeResponse(out);
|
||||
|
||||
//TODO: Change the execution flow of this method
|
||||
throw new IOException("Change this code please");
|
||||
}
|
||||
|
||||
sendStartStreamResponse(mediaQueue, out);
|
||||
|
||||
mediaQueue.startProcessor();
|
||||
mediaQueue.getMediaQueueProcessor().addMediaQueueProcessorListener(metadataManager);
|
||||
|
||||
metadataManager.setMetadata(mediaQueue.getMediaQueueProcessor().getCurrentMetadata());
|
||||
addClientConnectionListener(mediaQueue.getMediaQueueProcessor());
|
||||
|
||||
fireClientStartsStreaming();
|
||||
|
||||
FrameStorage storage = mediaQueue.getFrameStorage();
|
||||
|
||||
FrameStorageEntry entry = null;
|
||||
long time = System.currentTimeMillis();
|
||||
int bytesSent = 0;
|
||||
int total = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
entry = storage.find(time);
|
||||
time = entry.getStopTime();
|
||||
|
||||
// grab the next frame to send
|
||||
Frame frame = entry.getFrame();
|
||||
|
||||
// Check if we have to send metadata
|
||||
if (bytesSent + frame.getSize() >= metadataInterval) {
|
||||
|
||||
int sendBefore = metadataInterval - bytesSent;
|
||||
if (sendBefore > 0) {
|
||||
out.write(frame.getData(), 0, sendBefore);
|
||||
}
|
||||
|
||||
byte [] metadata = metadataManager.getMetaData(mediaQueue);
|
||||
out.write(metadata);
|
||||
|
||||
out.write(frame.getData(), sendBefore, frame.getSize()-sendBefore);
|
||||
bytesSent -= metadataInterval;
|
||||
} else {
|
||||
// we don't need to send any meta data
|
||||
//log.debu
|
||||
out.write(entry.getFrame().getData(), 0, entry.getFrame().getSize());
|
||||
}
|
||||
|
||||
bytesSent += frame.getSize();
|
||||
total += frame.getSize();
|
||||
|
||||
} catch (FrameStorageIsEmptyException e) {
|
||||
// sleep for a while and let the MediaQueueProcessor fill the storage
|
||||
sleep(250);
|
||||
} catch (FrameIsTooOldException e) {
|
||||
// the client is lagging behind. Reset the time to the current system time.
|
||||
time = System.currentTimeMillis();
|
||||
} catch (FrameHasNotBeenLoadedException e) {
|
||||
// the client is too far ahead.
|
||||
//log.info("Client is too far ahead, sleeping 250 Ms");
|
||||
sleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
// TODO: Fix error handling
|
||||
} catch(RuntimeException e) {
|
||||
e.printStackTrace();
|
||||
// TODO: Fix error handling
|
||||
}
|
||||
|
||||
fireClientStopsStreaming();
|
||||
|
||||
setExit(true);
|
||||
log.info(Thread.currentThread().getName() + " stopped");
|
||||
}
|
||||
|
||||
private void writeNegativeResponse(OutputStream out) throws IOException {
|
||||
out.write( (new String("404 Not found")).getBytes() );
|
||||
}
|
||||
|
||||
private void sendStartStreamResponse(MediaQueue mediaQueue, OutputStream out) throws IOException {
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("HTTP/1.1 200 OK\r\nContent-Type: audio/mpeg\r\n");
|
||||
|
||||
// add the stream name
|
||||
response.append("icy-name: ");
|
||||
response.append(mediaQueue.getName());
|
||||
response.append("\r\n");
|
||||
|
||||
// add metadata information
|
||||
response.append("icy-metadata:1\r\n");
|
||||
response.append("icy-metaint:");
|
||||
response.append(metadataInterval);
|
||||
response.append("\r\n");
|
||||
|
||||
response.append("\r\n");
|
||||
|
||||
out.write(response.toString().getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param in
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private HttpRequest readRequest(InputStream in) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
byte [] buffer = new byte[4096];
|
||||
int bytes;
|
||||
|
||||
byte endSequence[] = new byte[] {13,10,13,10};
|
||||
|
||||
while ( (bytes=in.read(buffer)) > 0) {
|
||||
sb.append(new String(buffer, 0, bytes));
|
||||
|
||||
if (bytes > endSequence.length) {
|
||||
boolean foundSequence = true;
|
||||
for (int i=0;i<endSequence.length;i++) {
|
||||
foundSequence |= (endSequence[i] == buffer[bytes-i-1]);
|
||||
}
|
||||
|
||||
if (foundSequence) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new HttpRequest( sb.toString() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the client thread with the default name 'ShoutClientThread'.
|
||||
*
|
||||
*/
|
||||
public void start() {
|
||||
start("ShoutClientThread");
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the client thread.
|
||||
* @param threadName Name of the thread
|
||||
*/
|
||||
public void start(String threadName) {
|
||||
Thread thread = new Thread(this, threadName);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public synchronized void setExit(boolean exit) {
|
||||
this.exit = exit;
|
||||
}
|
||||
|
||||
public synchronized boolean isExit() {
|
||||
return exit;
|
||||
}
|
||||
|
||||
public void addClientConnectionListener(ClientListener listener) {
|
||||
clientConnectionListeners .add(new WeakReference<ClientListener>(listener));
|
||||
}
|
||||
|
||||
public void removeClientConnectionListener(ClientListener listener) {
|
||||
clientConnectionListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private void fireClientStopsStreaming() {
|
||||
Iterator<WeakReference<ClientListener>> iterator = clientConnectionListeners.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
WeakReference<ClientListener> ref = iterator.next();
|
||||
|
||||
if (null == ref.get()) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
ref.get().clientStopsStreaming();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private void fireClientStartsStreaming() {
|
||||
Iterator<WeakReference<ClientListener>> iterator = clientConnectionListeners.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
WeakReference<ClientListener> ref = iterator.next();
|
||||
|
||||
if (null == ref.get()) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
ref.get().clientStartStreaming();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for a number of milliseconds and encapsule any exception in a EncapsuledExceptionRuntimException.
|
||||
* @param ms Number of milliseconds to sleep
|
||||
*/
|
||||
private void sleep(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
throw new EncapsuledExceptionRuntimException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/main/java/se/slackers/locality/shout/ShoutServer.java
Normal file
90
src/main/java/se/slackers/locality/shout/ShoutServer.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.shout.manager.ShoutRequestManager;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class ShoutServer implements Runnable {
|
||||
private static Logger log = Logger.getLogger(ShoutServer.class);
|
||||
private ServerSocket serverSocket;
|
||||
private ShoutThreadPool threadPool;
|
||||
private ShoutRequestManager requestManager;
|
||||
|
||||
protected int serverPort = 8001;
|
||||
protected boolean exitServer;
|
||||
|
||||
public void start() {
|
||||
Thread thread = new Thread(this, "SoutServer");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
log.info("Server started");
|
||||
|
||||
//.. create a listening socket
|
||||
try {
|
||||
serverSocket = new ServerSocket(serverPort);
|
||||
} catch (IOException e) {
|
||||
log.error(e);
|
||||
log.info("Server stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
//.. create the threads
|
||||
threadPool = new ShoutThreadPool(5, "ShoutThread-");
|
||||
|
||||
//.. main loop
|
||||
setExitServer(false);
|
||||
while (!isExitServer()) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
|
||||
ShoutRunnable client = new ShoutRunnable(requestManager, clientSocket);
|
||||
threadPool.startShoutRunnable(client);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error(e);
|
||||
|
||||
sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
threadPool.shutdown();
|
||||
|
||||
setExitServer(true);
|
||||
log.info("Server stopped");
|
||||
}
|
||||
|
||||
private void sleep(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setServerPort(int serverPort) {
|
||||
this.serverPort = serverPort;
|
||||
}
|
||||
|
||||
public synchronized boolean isExitServer() {
|
||||
return exitServer;
|
||||
}
|
||||
|
||||
public synchronized void setExitServer(boolean exitServer) {
|
||||
this.exitServer = exitServer;
|
||||
}
|
||||
|
||||
public void setRequestManager(ShoutRequestManager requestManager) {
|
||||
this.requestManager = requestManager;
|
||||
}
|
||||
}
|
||||
87
src/main/java/se/slackers/locality/shout/ShoutThread.java
Normal file
87
src/main/java/se/slackers/locality/shout/ShoutThread.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
public class ShoutThread extends Thread implements Runnable {
|
||||
private static Logger log = Logger.getLogger(ShoutThread.class);
|
||||
|
||||
private Object targetLock = new Object();
|
||||
|
||||
protected List<ShoutThreadListener> listeners = new ArrayList<ShoutThreadListener>();
|
||||
protected ShoutRunnable target = null;
|
||||
protected boolean exit = false;
|
||||
|
||||
public ShoutThread(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
log.debug("Thread "+this.getName()+" is started");
|
||||
|
||||
while (!isExit()) {
|
||||
synchronized (targetLock) {
|
||||
try {
|
||||
log.debug(this.getName()+": Waiting for task");
|
||||
targetLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Error when waiting for lock");
|
||||
log.error(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
assert target != null : "The ShoutRunnable is null for thread "+this.getName();
|
||||
|
||||
try {
|
||||
log.debug(this.getName()+": Running task");
|
||||
target.run();
|
||||
} catch(Exception e) {
|
||||
log.error("Thread "+this.getName()+" was aborted due to an exception", e);
|
||||
}
|
||||
|
||||
synchronized(targetLock) {
|
||||
target = null;
|
||||
}
|
||||
|
||||
log.debug(this.getName()+": Task complete, notifying listeners");
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
log.debug("Thread "+this.getName()+" is stopped");
|
||||
}
|
||||
|
||||
public void addShoutThreadListener(ShoutThreadListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
private void notifyListeners() {
|
||||
for (ShoutThreadListener listener : listeners) {
|
||||
listener.shoutThreadComplete(this);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isExit() {
|
||||
return exit;
|
||||
}
|
||||
|
||||
public synchronized void setExit(boolean exit) {
|
||||
this.exit = exit;
|
||||
|
||||
synchronized(targetLock) {
|
||||
if (target != null) {
|
||||
target.setExit(exit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTarget(ShoutRunnable target) {
|
||||
synchronized(targetLock) {
|
||||
this.target = target;
|
||||
targetLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
public interface ShoutThreadListener {
|
||||
public void shoutThreadComplete(ShoutThread thread);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
public class ShoutThreadPool implements ShoutThreadListener {
|
||||
private static Logger log = Logger.getLogger(ShoutThreadPool.class);
|
||||
|
||||
private List<ShoutThread> available = new ArrayList<ShoutThread>();
|
||||
|
||||
private List<ShoutThread> running = new ArrayList<ShoutThread>();
|
||||
|
||||
public ShoutThreadPool(int size, String prefix) {
|
||||
log.debug("Starting threads");
|
||||
for (int i = 0; i < size; i++) {
|
||||
ShoutThread thread = new ShoutThread(prefix + i);
|
||||
thread.addShoutThreadListener(this);
|
||||
thread.start();
|
||||
available.add(thread);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
for (ShoutThread thread : available) {
|
||||
thread.setExit(true);
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
for (ShoutThread thread : running) {
|
||||
thread.setExit(true);
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void shoutThreadComplete(ShoutThread thread) {
|
||||
log.debug("Recycling thread "+thread.getName());
|
||||
running.remove(thread);
|
||||
available.add(thread);
|
||||
}
|
||||
|
||||
public synchronized void startShoutRunnable(ShoutRunnable runnable) {
|
||||
if (available.isEmpty()) {
|
||||
throw new RuntimeException("No available threads in pool");
|
||||
}
|
||||
|
||||
ShoutThread thread = available.remove(0);
|
||||
running.add(thread);
|
||||
|
||||
log.debug("Using thread "+thread.getName()+" for execution");
|
||||
|
||||
thread.setExit(false);
|
||||
thread.setTarget(runnable);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package se.slackers.locality.shout.manager;
|
||||
|
||||
import se.slackers.locality.media.queue.MediaQueue;
|
||||
import se.slackers.locality.net.HttpRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public interface ShoutRequestManager {
|
||||
/**
|
||||
* Returns a MediaQueue object for the request or throws an exception.
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
public MediaQueue processRequest(HttpRequest request) throws SecurityException;
|
||||
|
||||
/**
|
||||
* Registers a new MediaQueue.
|
||||
* @param mediaQueue
|
||||
*/
|
||||
public void registerQueue(MediaQueue mediaQueue);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package se.slackers.locality.shout.manager;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import se.slackers.locality.exception.IllegalRequestException;
|
||||
import se.slackers.locality.media.queue.MediaQueue;
|
||||
import se.slackers.locality.net.HttpRequest;
|
||||
|
||||
/**
|
||||
* Handles the mapping between a request and a MediaQueue.
|
||||
* @author bysse
|
||||
*
|
||||
*/
|
||||
public class ShoutRequestManagerImpl implements ShoutRequestManager {
|
||||
private static final Logger log = Logger
|
||||
.getLogger(ShoutRequestManagerImpl.class);
|
||||
|
||||
private Map<String, MediaQueue> mountpoints = new HashMap<String, MediaQueue>();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public MediaQueue processRequest(HttpRequest request)
|
||||
throws SecurityException {
|
||||
try {
|
||||
String path = request.getRequestPath();
|
||||
if (mountpoints.containsKey(path)) {
|
||||
return mountpoints.get(path);
|
||||
}
|
||||
} catch (IllegalRequestException e) {
|
||||
log.error("IllegalRequest ["+request+"]");
|
||||
}
|
||||
|
||||
throw new SecurityException("The media queue "+request+" could not be found");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void registerQueue(MediaQueue mediaQueue) {
|
||||
String mountPoint = mediaQueue.getMountPoint();
|
||||
|
||||
if (mountpoints.containsKey(mountPoint)) {
|
||||
mountpoints.remove(mountPoint);
|
||||
}
|
||||
|
||||
mountpoints.put(mountPoint, mediaQueue);
|
||||
}
|
||||
}
|
||||
16
src/main/java/se/slackers/locality/util/OneShot.java
Normal file
16
src/main/java/se/slackers/locality/util/OneShot.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package se.slackers.locality.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class OneShot {
|
||||
private static Map<String, Boolean> map = new HashMap<String, Boolean>();
|
||||
|
||||
public static boolean test(String id) {
|
||||
if (map.containsKey(id)) {
|
||||
return false;
|
||||
}
|
||||
map.put(id, Boolean.TRUE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
89
src/main/resources/application-context/data.xml
Executable file
89
src/main/resources/application-context/data.xml
Executable file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
|
||||
|
||||
|
||||
<!-- =================================================================== -->
|
||||
<!-- Database configuration -->
|
||||
<!-- =================================================================== -->
|
||||
<bean id="dataSource"
|
||||
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
|
||||
lazy-init="true" p:driverClassName="${db.driverClassName}"
|
||||
p:url="${db.url}" p:username="${db.username}"
|
||||
p:password="${db.password}" p:defaultTransactionIsolation="2"
|
||||
p:initialSize="50" p:maxActive="50" p:maxWait="10000"
|
||||
p:poolPreparedStatements="true" />
|
||||
|
||||
<bean id="jdbcTemplate"
|
||||
class="org.springframework.jdbc.core.JdbcTemplate"
|
||||
p:data-source-ref="dataSource" />
|
||||
|
||||
<!-- =================================================================== -->
|
||||
<!-- Hibernate configuration -->
|
||||
<!-- =================================================================== -->
|
||||
<bean id="sessionFactory"
|
||||
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
|
||||
p:dataSource-ref="dataSource">
|
||||
<property name="annotatedClasses">
|
||||
<list>
|
||||
<value>se.slackers.locality.model.MetaTag</value>
|
||||
<value>se.slackers.locality.model.Tag</value>
|
||||
</list>
|
||||
</property>
|
||||
<property name="annotatedPackages">
|
||||
<list>
|
||||
<value>se.slackers.locality.model</value>
|
||||
</list>
|
||||
</property>
|
||||
<property name="hibernateProperties">
|
||||
<props>
|
||||
<prop key="hibernate.dialect">
|
||||
${db.hibernate.dialect}
|
||||
</prop>
|
||||
<prop key="hibernate.jdbc.batch_size">20</prop>
|
||||
<prop key="show_sql">true</prop>
|
||||
<prop key="hibernate.hbm2ddl.auto">create</prop>
|
||||
<prop key="hibernate.max_fetch_depth">2</prop>
|
||||
<prop key="hibernate.cache.provider_class">
|
||||
org.hibernate.cache.NoCacheProvider
|
||||
</prop>
|
||||
<prop key="hibernate.show_sql">true</prop>
|
||||
<prop key="hibernate.autoReconnect">true</prop>
|
||||
<prop key="hibernate.autoReconnectForPools">true</prop>
|
||||
<prop
|
||||
key="hibernate.is-connection-validation-required">
|
||||
false
|
||||
</prop>
|
||||
|
||||
<!--
|
||||
<prop key="hibernate.cache.use_query_cache">true</prop>
|
||||
<prop key="hibernate.cache.provider_class">
|
||||
org.hibernate.cache.EhCacheProvider
|
||||
</prop>
|
||||
-->
|
||||
</props>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="transactionManager"
|
||||
class="org.springframework.orm.hibernate3.HibernateTransactionManager"
|
||||
p:sessionFactory-ref="sessionFactory" />
|
||||
|
||||
<bean id="hibernateTemplate"
|
||||
class="org.springframework.orm.hibernate3.HibernateTemplate">
|
||||
<property name="sessionFactory" ref="sessionFactory" />
|
||||
</bean>
|
||||
|
||||
<!-- =================================================================== -->
|
||||
<!-- DAO configuration -->
|
||||
<!-- =================================================================== -->
|
||||
<bean id="tagDao"
|
||||
class="se.slackers.locality.dao.hibernate.TagDaoImpl"
|
||||
p:hibernateTemplate-ref="hibernateTemplate" />
|
||||
|
||||
<bean id="metaTagDao"
|
||||
class="se.slackers.locality.dao.hibernate.MetaTagDaoImpl"
|
||||
p:hibernateTemplate-ref="hibernateTemplate" />
|
||||
</beans>
|
||||
13
src/main/resources/application-context/main.xml
Executable file
13
src/main/resources/application-context/main.xml
Executable file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
|
||||
|
||||
<!-- property placeholder post-processor -->
|
||||
<bean id="placeholderConfig"
|
||||
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
|
||||
<property name="location"
|
||||
value="classpath:/conf.properties" />
|
||||
</bean>
|
||||
</beans>
|
||||
14
src/main/resources/application-context/shout.xml
Normal file
14
src/main/resources/application-context/shout.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
|
||||
|
||||
<bean name="requestManager"
|
||||
class="se.slackers.locality.shout.manager.ShoutRequestManagerImpl" />
|
||||
|
||||
<bean name="shoutServer"
|
||||
class="se.slackers.locality.shout.ShoutServer"
|
||||
p:requestManager.ref="requestManager" p:serverPort="8001" />
|
||||
|
||||
</beans>
|
||||
5
src/main/resources/conf.properties
Executable file
5
src/main/resources/conf.properties
Executable file
@@ -0,0 +1,5 @@
|
||||
db.driverClassName=org.h2.Driver
|
||||
db.url=jdbc:h2:db/locality.db
|
||||
db.username=sa
|
||||
db.password=
|
||||
db.hibernate.dialect=org.hibernate.dialect.H2Dialect
|
||||
16
src/main/resources/log4j.properties
Executable file
16
src/main/resources/log4j.properties
Executable file
@@ -0,0 +1,16 @@
|
||||
log4j.rootCategory=DEBUG, CONSOLE, LOGFILE
|
||||
|
||||
log4j.category.org.apache.commons=ERROR
|
||||
|
||||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.CONSOLE.Threshold=DEBUG
|
||||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ddMM HH:mm} %-5p [%t] %c %x - %m%n
|
||||
|
||||
# LOGFILE is set to be a File appender using a PatternLayout.
|
||||
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
|
||||
log4j.appender.LOGFILE.File=logs/locality.log
|
||||
log4j.appender.LOGFILE.Append=true
|
||||
log4j.appender.LOGFILE.Threshold=DEBUG
|
||||
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ddMMyyyy HH:mm:ss,SSS} %-5p [%t] %c %x - %m%n
|
||||
43
src/main/resources/schemas/locality.xsd
Executable file
43
src/main/resources/schemas/locality.xsd
Executable file
@@ -0,0 +1,43 @@
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:hr="http://slackers.se/hr/schemas" elementFormDefault="qualified"
|
||||
targetNamespace="http://slackers.se/hr/schemas">
|
||||
<!--
|
||||
<xs:element name="HolidayRequest">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="Holiday" type="hr:HolidayType" />
|
||||
<xs:element name="Employee" type="hr:EmployeeType" />
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
-->
|
||||
|
||||
<xs:complexType name="MetaTagType">
|
||||
<xs:sequence>
|
||||
<xs:element name="Name" type="xs:string" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="TagType">
|
||||
<xs:sequence>
|
||||
<xs:element name="Name" type="xs:string" />
|
||||
<xs:element name="MetaTags" maxOccurs="1" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="MetaTag"
|
||||
type="MetaTagType">
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="EmployeeType">
|
||||
<xs:sequence>
|
||||
<xs:element name="Number" type="xs:integer" />
|
||||
<xs:element name="FirstName" type="xs:string" />
|
||||
<xs:element name="LastName" type="xs:string" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
47
src/test/java/se/slackers/locality/dao/MetaTagDaoTest.java
Executable file
47
src/test/java/se/slackers/locality/dao/MetaTagDaoTest.java
Executable file
@@ -0,0 +1,47 @@
|
||||
package se.slackers.locality.dao;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
|
||||
|
||||
import se.slackers.locality.model.MetaTag;
|
||||
|
||||
public class MetaTagDaoTest extends AbstractTransactionalDataSourceSpringContextTests {
|
||||
|
||||
protected MetaTagDao tagDao;
|
||||
protected SessionFactory sessionFactory = null;
|
||||
|
||||
public void testSave() {
|
||||
MetaTag tag = new MetaTag();
|
||||
tag.setName("testMetaTag");
|
||||
tagDao.save(tag);
|
||||
|
||||
MetaTag fetchedMetaTag = tagDao.get("testMetaTag");
|
||||
assertEquals(tag.getId(), fetchedMetaTag.getId());
|
||||
}
|
||||
|
||||
public void testDelete() {
|
||||
MetaTag tag = new MetaTag();
|
||||
tag.setName("testMetaTag");
|
||||
tagDao.save(tag);
|
||||
|
||||
tagDao.delete(tag);
|
||||
try {
|
||||
tagDao.get("testMetaTag");
|
||||
fail();
|
||||
} catch (DataRetrievalFailureException e) {
|
||||
}
|
||||
}
|
||||
|
||||
protected String[] getConfigLocations() {
|
||||
return new String[] { "application-context/main.xml", "application-context/data.xml" };
|
||||
}
|
||||
|
||||
public void setMetaTagDao(MetaTagDao tagDao) {
|
||||
this.tagDao = tagDao;
|
||||
}
|
||||
|
||||
public void setSessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
}
|
||||
47
src/test/java/se/slackers/locality/dao/TagDaoTest.java
Executable file
47
src/test/java/se/slackers/locality/dao/TagDaoTest.java
Executable file
@@ -0,0 +1,47 @@
|
||||
package se.slackers.locality.dao;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
|
||||
|
||||
import se.slackers.locality.model.Tag;
|
||||
|
||||
public class TagDaoTest extends AbstractTransactionalDataSourceSpringContextTests {
|
||||
|
||||
protected TagDao tagDao;
|
||||
protected SessionFactory sessionFactory = null;
|
||||
|
||||
public void testSave() {
|
||||
Tag tag = new Tag();
|
||||
tag.setName("testTag");
|
||||
tagDao.save(tag);
|
||||
|
||||
Tag fetchedTag = tagDao.get("testTag");
|
||||
assertEquals(tag.getId(), fetchedTag.getId());
|
||||
}
|
||||
|
||||
public void testDelete() {
|
||||
Tag tag = new Tag();
|
||||
tag.setName("testMetaTag");
|
||||
tagDao.save(tag);
|
||||
|
||||
tagDao.delete(tag);
|
||||
try {
|
||||
tagDao.get("testMetaTag");
|
||||
fail();
|
||||
} catch (DataRetrievalFailureException e) {
|
||||
}
|
||||
}
|
||||
|
||||
protected String[] getConfigLocations() {
|
||||
return new String[] { "application-context/main.xml", "application-context/data.xml" };
|
||||
}
|
||||
|
||||
public void setTagDao(TagDao tagDao) {
|
||||
this.tagDao = tagDao;
|
||||
}
|
||||
|
||||
public void setSessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
}
|
||||
45
src/test/java/se/slackers/locality/dao/TagTest.java
Executable file
45
src/test/java/se/slackers/locality/dao/TagTest.java
Executable file
@@ -0,0 +1,45 @@
|
||||
package se.slackers.locality.dao;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
|
||||
|
||||
import se.slackers.locality.model.MetaTag;
|
||||
import se.slackers.locality.model.Tag;
|
||||
|
||||
public class TagTest extends AbstractTransactionalDataSourceSpringContextTests {
|
||||
|
||||
protected TagDao tagDao;
|
||||
protected MetaTagDao metaTagDao;
|
||||
protected SessionFactory sessionFactory = null;
|
||||
|
||||
public void testManyToMany() {
|
||||
Tag tag = new Tag();
|
||||
MetaTag metatag = new MetaTag();
|
||||
|
||||
tag.setName("tag");
|
||||
metatag.setName("metatag");
|
||||
metatag.addTag(tag);
|
||||
|
||||
tagDao.save(tag);
|
||||
metaTagDao.save(metatag);
|
||||
|
||||
System.out.println(tagDao.get(tag.getId()));
|
||||
System.out.println(metaTagDao.get(metatag.getId()));
|
||||
}
|
||||
|
||||
protected String[] getConfigLocations() {
|
||||
return new String[] { "application-context/main.xml", "application-context/data.xml" };
|
||||
}
|
||||
|
||||
public void setTagDao(TagDao tagDao) {
|
||||
this.tagDao = tagDao;
|
||||
}
|
||||
|
||||
public void setMetaTagDao(MetaTagDao metaTagDao) {
|
||||
this.metaTagDao = metaTagDao;
|
||||
}
|
||||
|
||||
public void setSessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
}
|
||||
100
src/test/java/se/slackers/locality/data/CircularBufferTest.java
Normal file
100
src/test/java/se/slackers/locality/data/CircularBufferTest.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
public class CircularBufferTest extends TestCase {
|
||||
private CircularBuffer buffer = new CircularBuffer(1000);
|
||||
private byte [] temp = new byte[1000];
|
||||
|
||||
@Test
|
||||
public void testReadEmpty() {
|
||||
buffer.reset();
|
||||
assertEquals(0, buffer.read(temp, 0, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteRead() {
|
||||
buffer.reset();
|
||||
int length = 100;
|
||||
assertEquals(true, buffer.write(temp, 0, length));
|
||||
assertEquals(length, buffer.read(temp, 0, length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteReadOverflow() {
|
||||
buffer.reset();
|
||||
int length = 100;
|
||||
assertEquals(true, buffer.write(temp, 0, length));
|
||||
assertEquals(length, buffer.read(temp, 0, length*2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteOverEdge() {
|
||||
buffer.reset();
|
||||
assertEquals(true, buffer.write(temp, 0, 900));
|
||||
assertEquals(100, buffer.read(temp, 0, 100));
|
||||
// should be 200 bytes free in buffer
|
||||
assertEquals(true, buffer.write(temp, 0, 150));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteOverEdgeLimit() {
|
||||
buffer.reset();
|
||||
assertEquals(true, buffer.write(temp, 0, 900));
|
||||
assertEquals(100, buffer.read(temp, 0, 100));
|
||||
// should be 200 bytes free in buffer
|
||||
assertEquals(false, buffer.write(temp, 0, 250));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOverEdge() {
|
||||
buffer.reset();
|
||||
assertEquals(true, buffer.write(temp, 0, 900));
|
||||
assertEquals(800, buffer.read(temp, 0, 800));
|
||||
// readIndex = 800, writeIndex = 900
|
||||
assertEquals(true, buffer.write(temp, 0, 500));
|
||||
assertEquals(500, buffer.read(temp, 0, 500));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOverEdgeLimit() {
|
||||
buffer.reset();
|
||||
assertEquals(true, buffer.write(temp, 0, 900));
|
||||
assertEquals(800, buffer.read(temp, 0, 800));
|
||||
// readIndex = 800, writeIndex = 900
|
||||
assertEquals(true, buffer.write(temp, 0, 500));
|
||||
assertEquals(600, buffer.read(temp, 0, 700));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRandomness() {
|
||||
buffer.reset();
|
||||
int size = 0;
|
||||
|
||||
for (int i=0;i<100;i++) {
|
||||
size = 1000;
|
||||
while (true) {
|
||||
int len = (int)(Math.random()*400.0+100.0);
|
||||
if (!buffer.write(temp, 0, len))
|
||||
break;
|
||||
size -= len;
|
||||
}
|
||||
|
||||
assertTrue(size >= 0);
|
||||
|
||||
size = 1000-size;
|
||||
while (true) {
|
||||
int len = (int)(Math.random()*400.0+100.0);
|
||||
int bytes = buffer.read(temp, 0, len);
|
||||
size -= bytes;
|
||||
if (0 == bytes)
|
||||
break;
|
||||
}
|
||||
|
||||
assertTrue(size == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
public class ExpandOnWriteCircularBufferTest extends TestCase {
|
||||
private static final int SIZE = 1000;
|
||||
private ExpandOnWriteCircularBuffer buffer = new ExpandOnWriteCircularBuffer(SIZE);
|
||||
private byte [] temp = new byte[SIZE];
|
||||
|
||||
@Test
|
||||
public void testReadEmpty() {
|
||||
buffer.reset();
|
||||
assertEquals(0, buffer.read(0, temp, 0, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteRead() {
|
||||
buffer.reset();
|
||||
int length = 100;
|
||||
assertEquals(true, buffer.write(temp, 0, length));
|
||||
assertEquals(length, buffer.read(0, temp, 0, length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRandomness() {
|
||||
buffer.reset();
|
||||
int size = 0;
|
||||
|
||||
for (int i=0;i<100;i++) {
|
||||
size = 0;
|
||||
for (int j=0;j<10;j++) {
|
||||
int len = (int)(Math.random()*400.0+100.0);
|
||||
buffer.write(temp, 0, len);
|
||||
size += len;
|
||||
}
|
||||
|
||||
assertTrue(size-SIZE+1 == buffer.getReadOffset());
|
||||
|
||||
buffer.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import se.slackers.locality.media.Frame;
|
||||
|
||||
|
||||
public class FixedFrameSizeFrameStorageTest {
|
||||
|
||||
@Test
|
||||
public void testInsertUpdate() {
|
||||
FixedFrameSizeFrameStorage storage = new FixedFrameSizeFrameStorage();
|
||||
|
||||
// fill the storage with entries
|
||||
long time = 0;
|
||||
for (int i=0;i<100;i++) {
|
||||
storage.add(makeEntry(time));
|
||||
time += 26;
|
||||
}
|
||||
|
||||
FrameStorageEntry entry = storage.find(6*26+3);
|
||||
String value = new String(entry.getFrame().getData());
|
||||
assertEquals("10011100", value);
|
||||
|
||||
storage.purgeUntil(10*26);
|
||||
try {
|
||||
storage.find(10*26-1);
|
||||
fail();
|
||||
} catch (RuntimeException e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private FrameStorageEntry makeEntry(long time) {
|
||||
byte [] data = Long.toBinaryString(time).getBytes();
|
||||
|
||||
Frame frame = new Frame(data.length);
|
||||
frame.setLength(26);
|
||||
|
||||
System.arraycopy(data, 0, frame.getData(), 0, data.length);
|
||||
frame.setSize(data.length);
|
||||
|
||||
return new FrameStorageEntry(time, frame);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package se.slackers.locality.data;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import se.slackers.locality.model.Metadata;
|
||||
|
||||
|
||||
public class MetadataManagerTest {
|
||||
private MetadataManager mm = new MetadataManager();
|
||||
|
||||
|
||||
@Test
|
||||
public void testParse() {
|
||||
Metadata info = Metadata.create("ARTIST", "ALBUM", "TITLE");
|
||||
|
||||
String result = mm.parseFormat("${artist} $album$track ?(title,-) ${title}", info);
|
||||
assertEquals("ARTIST ALBUM - TITLE", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package se.slackers.locality.media.reader.mp3;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import se.slackers.locality.media.Frame;
|
||||
import se.slackers.locality.model.Media;
|
||||
|
||||
public class Mp3MediaReaderTest extends TestCase {
|
||||
|
||||
private static final File base = new File("target/test-classes");
|
||||
private Media media = null;
|
||||
private Mp3MediaReader reader = null;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
media = new Media();
|
||||
media.setMediaFile(new File(base, "test.mp3"));
|
||||
|
||||
reader = new Mp3MediaReader();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead() {
|
||||
try {
|
||||
reader.open(media);
|
||||
Frame frame = new Frame(4096);
|
||||
|
||||
reader.readFrame(frame);
|
||||
reader.readFrame(frame);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/test/java/se/slackers/locality/net/HttpRequestTest.java
Normal file
19
src/test/java/se/slackers/locality/net/HttpRequestTest.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package se.slackers.locality.net;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import se.slackers.locality.net.HttpRequest;
|
||||
|
||||
|
||||
|
||||
public class HttpRequestTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testRequest() throws Exception {
|
||||
String httpRequest = "GET /media.mp3 HTTP/1.0\n\rHost: localhost\n\rUser-Agent: xmms/1.2.10\n\rIcy-MetaData:1\n\r\n\r";
|
||||
HttpRequest request = new HttpRequest(httpRequest);
|
||||
assertEquals("/media.mp3", request.getRequestPath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package se.slackers.locality.shout;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import se.slackers.locality.data.FixedFrameSizeFrameStorage;
|
||||
import se.slackers.locality.media.queue.MediaQueue;
|
||||
import se.slackers.locality.media.queue.MediaQueueImpl;
|
||||
import se.slackers.locality.media.queue.MediaQueueProcessor;
|
||||
import se.slackers.locality.media.queue.PreloadDataMediaQueueProcessor;
|
||||
import se.slackers.locality.media.reader.MediaReaderFactory;
|
||||
import se.slackers.locality.media.reader.mp3.Mp3MediaReader;
|
||||
import se.slackers.locality.model.Media;
|
||||
import se.slackers.locality.shout.manager.ShoutRequestManager;
|
||||
import se.slackers.locality.shout.manager.ShoutRequestManagerImpl;
|
||||
|
||||
|
||||
public class ShoutServerTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testServerStart() {
|
||||
Media media = new Media();
|
||||
media.setMediaFile(new File("test.mp3"));
|
||||
|
||||
MediaReaderFactory mediaReaderFactory = new MediaReaderFactory();
|
||||
mediaReaderFactory.addMediaReader(new Mp3MediaReader());
|
||||
|
||||
MediaQueue queue = new MediaQueueImpl("mediaQueue");
|
||||
queue.setMountPoint("/media.mp3");
|
||||
queue.add(media);
|
||||
|
||||
MediaQueueProcessor processor = new PreloadDataMediaQueueProcessor();
|
||||
processor.setMediaQueue(queue);
|
||||
processor.setMediaReaderFactory(mediaReaderFactory);
|
||||
|
||||
queue.setMediaQueueProcessor(processor);
|
||||
queue.setFrameStorage( new FixedFrameSizeFrameStorage() );
|
||||
|
||||
ShoutRequestManager manager = new ShoutRequestManagerImpl();
|
||||
manager.registerQueue(queue);
|
||||
|
||||
ShoutServer server = new ShoutServer();
|
||||
server.setServerPort(8001);
|
||||
|
||||
server.setRequestManager(manager);
|
||||
server.run();
|
||||
}
|
||||
}
|
||||
BIN
src/test/resources/silence.mp3
Normal file
BIN
src/test/resources/silence.mp3
Normal file
Binary file not shown.
BIN
src/test/resources/test.mp3
Normal file
BIN
src/test/resources/test.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user