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