/*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is the Kowari Metadata Store.
*
* The Initial Developer of the Original Code is Plugged In Software Pty
* Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions
* created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002
* Plugged In Software Pty Ltd. All Rights Reserved.
*
* Contributor(s): N/A.5
*
* [NOTE: The text of this Exhibit A may differ slightly from the text
* of the notices in the Source Code files of the Original Code. You
* should use the text of this Exhibit A rather than the text found in the
* Original Code Source Code for Your Modifications.]
*
*/
package org.mulgara.content.mp3;
// Java 2 standard packages
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
// Apache Log4J
import org.apache.log4j.Logger;
// JRDF
import org.jrdf.graph.*;
import org.jrdf.graph.mem.*;
import org.jrdf.util.ClosableIterator;
// Mp3 Library
import org.blinkenlights.id3.*;
// Locally written packages
import org.mulgara.content.Content;
import org.mulgara.content.NotModifiedException;
import org.mulgara.content.mp3.parser.*;
import org.mulgara.content.mp3.parser.api.*;
import org.mulgara.content.mp3.parser.exception.*;
import org.mulgara.query.TuplesException;
import org.mulgara.query.Variable;
import org.mulgara.resolver.spi.LocalizeException;
import org.mulgara.resolver.spi.ResolverSession;
import org.mulgara.resolver.spi.Statements;
import org.mulgara.store.tuples.AbstractTuples;
import org.mulgara.store.tuples.Tuples;
import org.mulgara.util.TempDir;
/**
* Parses an MP3's ID3 tag into {@link Statements};
*
* @created 2004-08-13
*
* @author Mark Ludlow
*
* @version $Revision: 1.10 $
*
* @modified $Date: 2005/02/02 21:13:04 $ @maintenanceAuthor $Author: newmana $
*
* @company <a href="mailto:info@PIsoftware.com">Plugged In Software</a>
*
* @copyright © 2004 <a href="http://www.PIsoftware.com/">Plugged In
* Software Pty Ltd</a>
*
* @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a>
*/
public class MP3Statements extends AbstractTuples implements Statements {
/** Logger. */
private static final Logger logger =
Logger.getLogger(MP3Statements.class.getName());
/** Column index for subjects */
public static final int SUBJECT = 0;
/** Column index for predicates */
public static final int PREDICATE = 1;
/** Column index for predicates */
public static final int OBJECT = 2;
/** The session used to globalize the RDF nodes from the stream. */
private ResolverSession resolverSession;
/** The number of statements in the ID3 tag. */
private long rowCount;
/** The current row. If the cursor is not on a row, this will be <code>null</code> */
private Triple tripleStatement;
/** An interator into triples positioned at the next triple. */
private ClosableIterator<Triple> nextTriple;
/** The content representing the MP3 file */
private Content content;
/** The model which will store the content of parsed mp3 files */
private Graph model;
/**
* Map ARP anonymous node IDs to {@link BlankNode}s.
*
* This is <code>null</code> if no parsing is in progress.
*/
private Map<Node,Long> blankNodeMap = new HashMap<Node,Long>();
//
// Constructors
//
/**
* Construct an RDF/XML stream parser.
*
* @param content the content object representing our MP3 file
* @param resolverSession session against which to localize RDF nodes
* @throws IllegalArgumentException if <var>inputStream</var> or
* <var>resolverSession</var> are <code>null</code>
* @throws NotModifiedException if there's a valid cached copy of these
* statements
* @throws TuplesException if the <var>inputStream</var> can't be parsed as
* RDF/XML
*/
MP3Statements(Content content, ResolverSession resolverSession) throws
NotModifiedException, TuplesException {
// Validate "content" parameter
if (content == null) {
throw new IllegalArgumentException("Null \"content\" parameter");
}
// Validate "resolverSession" parameter
if (resolverSession == null) {
throw new IllegalArgumentException("Null \"resolverSession\" parameter");
}
// Initialize fields
this.content = content;
this.resolverSession = resolverSession;
// Fix the magical column names for RDF statements
setVariables(new Variable[] {new Variable("subject"),
new Variable("predicate"),
new Variable("object")});
try {
// Initialise the parser factory
ParserFactory.getInstance().initialiseFactory();
} catch (FactoryException factoryException) {
throw new TuplesException("Unable to initialise factory for parsers.",
factoryException);
}
// Find the blank node map of our content object
blankNodeMap = null;
// Load in the RDF conversion of the given mp3 content
loadURL();
}
/**
* Load in the RDF conversion from the content object.
*
* @throws NotModifiedException if a valid cached version of the URL exists
* @throws TuplesException
*/
private void loadURL() throws NotModifiedException, TuplesException {
if (content.getURI() == null) throw new TuplesException("Unable to load MP3 from a stream. Use a file.");
try {
// Initialise the model to be a memory based graph
model = new GraphImpl();
} catch (GraphException graphException) {
throw new TuplesException("Unable to create a new graph object.", graphException);
}
// Create a container for our file
File contentFile = null;
if (!content.getURI().getScheme().equals("file")) {
// If we are dealing with anything other than a file then use the caching process
try {
// Convert the URI into a file
contentFile = getCachedFile(content.newInputStream(), content.getURI());
} catch (IOException ioException) {
throw new TuplesException(
"Unable to open a stream to the content file [" +
content.getURI().toString() + "]", ioException);
}
} else {
// Files are local and do not need caching
contentFile = new File(content.getURI());
}
// Parse the content of the file/directory to the model
parseFile(contentFile);
// Parse the stream into RDF statements
blankNodeMap = new HashMap<Node,Long>();
try {
// Initialize the metadata now that we know the statements
rowCount = model.getNumberOfTriples();
} catch (GraphException graphException) {
throw new TuplesException(
"Unable to retrieve number of triples in graph.",
graphException);
}
if (logger.isDebugEnabled()) logger.debug("Parsed MP3: Found " + rowCount + " triples");
}
//
// Methods implementing Statements
//
/**
* Retrieves the value contained in the subject column for the current triple.
*
* @return The subject value for the current triple
*
* @throws TuplesException
*/
public long getSubject() throws TuplesException {
return getColumnValue(SUBJECT);
}
/**
* Retrieves the value contained in the predicate column for the current triple.
*
* @return The predicate value for the current triple
*
* @throws TuplesException
*/
public long getPredicate() throws TuplesException {
return getColumnValue(PREDICATE);
}
/**
* Retrieves the value contained in the object column for the current triple.
*
* @return The object value for the current triple
*
* @throws TuplesException
*/
public long getObject() throws TuplesException {
return getColumnValue(OBJECT);
}
//
// Methods implementing AbstractTuples
//
/**
* Resets the counter for triples to be the first.
*
* @param prefix The prefix to use
* @param suffixTruncation The truncation of suffixes to use
*
* @throws TuplesException
*/
public void beforeFirst(long[] prefix, int suffixTruncation) throws TuplesException {
try {
// Get the iterator for statements in the model
nextTriple = model.find(null, null, null);
} catch (GraphException graphException) {
throw new TuplesException("Unable to retrieve triple iterator for graph.", graphException);
}
if (logger.isDebugEnabled()) {
try {
logger.debug("-- Getting the before first value from model " + model +
" which has statements " + nextTriple.hasNext() + " from " +
model.getNumberOfTriples() + " triples");
} catch (GraphException graphException) {
// Since we are debugging, it is not important if this exception is
// ignored
}
}
}
public Object clone() {
MP3Statements cloned = (MP3Statements)super.clone();
// Copy immutable fields by reference
cloned.resolverSession = resolverSession;
cloned.rowCount = rowCount;
cloned.tripleStatement = tripleStatement;
cloned.content = content;
return cloned;
}
/**
* Close the RDF/XML formatted input stream.
*/
public void close() throws TuplesException {
resolverSession = null;
tripleStatement = null;
content = null;
}
/**
* @param column 0 for the subject, 1 for the predicate, 2 for the object
*/
public long getColumnValue(int column) throws TuplesException {
// Pull the appropriate field from the current triple as a JRDF Node
Node node = null;
switch (column) {
case SUBJECT:
// Try creating the node with a URI reference
node = tripleStatement.getSubject();
break;
case PREDICATE:
// Try to create a URI reference node to represent the predicate
node = tripleStatement.getPredicate();
break;
case OBJECT:
// Create a literal node with the value for objects
node = tripleStatement.getObject();
break;
default:
throw new TuplesException("No such column " + column);
}
assert node != null;
// Container for our result
Long result = null;
if (blankNodeMap.containsKey(node)) {
// If the node is already mapped then get the value
result = blankNodeMap.get(node);
} else {
// If we haven't mapped the node already then create a new value and store it
// Localize the node and store the long object value
try {
result = new Long(resolverSession.localize(node));
} catch (LocalizeException e) {
throw new TuplesException("Couldn't get column " + column + " value", e);
}
// Store the new mapping
blankNodeMap.put(node, result);
}
if (column == SUBJECT && logger.isInfoEnabled()) {
logger.info("!! Using node value of: " + result.longValue());
}
return result.longValue();
}
public List<Tuples> getOperands() {
return Collections.emptyList();
}
public long getRowCount() throws TuplesException {
return rowCount;
}
public long getRowUpperBound() throws TuplesException {
return getRowCount();
}
public long getRowExpectedCount() throws TuplesException {
return getRowCount();
}
public boolean isEmpty() throws TuplesException {
return rowCount == 0;
}
public boolean hasNoDuplicates() throws TuplesException {
return false;
}
public boolean isColumnEverUnbound(int column) throws TuplesException {
switch (column) {
case 0:
case 1:
case 2:
return false;
default:
throw new TuplesException("No such column " + column);
}
}
public boolean next() throws TuplesException {
if (nextTriple.hasNext()) {
// Get the next statement in the iterator
tripleStatement = nextTriple.next();
if (logger.isDebugEnabled()) logger.debug("-- Getting next statement: " + tripleStatement.toString());
return true;
} else {
tripleStatement = null;
return false;
}
}
/**
* Checks whether the given file is a file or directory and then acts
* accordingly. It should not be confused with the parseFile method which
* does the actual conversion from an ID3 tag to RDF. This method is
* recursive so subdirectories will be navigated.
*
* @param file The file or directory we are checking the content of
*
* @throws TuplesException
*/
private void parseFile(File file) throws TuplesException {
if (file.getName().endsWith(".mp3")) {
// If the file is a valid mp3 file then parse the content into the model
// Container for our mp3 file
MP3File mp3File = null;
// Create a new MP3 file to represent our content
mp3File = new MP3File(file);
// container for our extended URI
URI escapedURI = null;
try {
// Create an extended version of the URI for the file
escapedURI = new URI(file.toURI().getScheme() + "://" + file.toURI().getRawPath());
} catch (URISyntaxException uriSyntaxException) {
throw new TuplesException("Failed to create a valid extended uri from" + file.toURI(), uriSyntaxException);
}
// Create a new conversion object
MP3Conversion conversion = new MP3Conversion(mp3File, model, escapedURI);
// Container for our parser object
ID3Parser parser = null;
try {
// Get a parser instance
parser = ParserFactory.getInstance().createID3Parser();
} catch (FactoryException factoryException) {
throw new TuplesException("Unable to create a new ID3Parser due to a factory error.", factoryException);
}
try {
// Parse the mp3 into the model
parser.parseTags(conversion);
} catch (ParserException parserException) {
throw new TuplesException("Unable to parse tags for file: " + file, parserException);
}
} else {
throw new TuplesException("Content object did not contain a valid mime type for parsing.");
}
}
/**
* Creates a locally cached version of a file from an input stream. If the
* file already exists then it will not download the file but instead use the
* cached version.
*
* @param inputStream The stream of data we are caching
* @param uri The uri of the data we are caching
*
* @return The file handle to the cached file
*
* @throws TuplesException
*/
private File getCachedFile(InputStream inputStream, URI uri) throws
TuplesException {
// Retrieve the path to the file on the remote host
String remotePath = uri.getPath();
// Retrieve the actual name of the file
String fileName = remotePath.substring(remotePath.lastIndexOf("/") + 1,
remotePath.length());
if (logger.isDebugEnabled()) {
logger.debug("Transferring [" + uri + "] to cached file [" + fileName + "]");
}
// Create a temporary cache directory handle
File cache = new File(TempDir.getTempDir(), "resolvercache");
if (!cache.exists()) {
// Check that the directory exists and if not then create it
cache.mkdirs();
}
if (!cache.isDirectory()) {
// If we can't use the directory name because a file has it, then just use
// the temporary directory
cache = TempDir.getTempDir();
}
// Create a new file representing the cached file
File file = new File(cache, fileName);
if (!file.exists()) {
// If the file does not exists in the cache already then download the file
// Container for our OutputStream to the file
OutputStream outStream = null;
try {
// Attempt to create an output stream to the output file
outStream = new FileOutputStream(file);
} catch (FileNotFoundException fileNotFoundException) {
throw new TuplesException("Unable to locate output file for caching " +
"of local version of: " + uri.toString(),
fileNotFoundException);
}
// Create an inputStream to read from
InputStreamReader reader = new InputStreamReader(inputStream);
// Container for the bytes in our stream
int nextByte = 0;
try {
// Get the first byte of the stream
nextByte = reader.read();
while (nextByte != -1) {
// Write out the current byte
outStream.write(nextByte);
// Read the next byte of the file
nextByte = reader.read();
}
} catch (IOException ioException) {
throw new TuplesException("Failed to transfer bytes from source to " +
"cache due to an I/O error.", ioException);
} finally {
try {
// Attempt to shutdown the output stream
outStream.flush();
outStream.close();
} catch (IOException ioException) {
throw new TuplesException("Failed to close output stream to cache",
ioException);
}
try {
// Attempt to close down the input stream reader
reader.close();
} catch (IOException ioException) {
throw new TuplesException("Failed to close input stream from " +
uri.toString(), ioException);
}
}
}
return file;
}
}