package org.jaudiotagger.audio.asf.io; import org.jaudiotagger.audio.asf.data.Chunk; import org.jaudiotagger.audio.asf.data.ChunkContainer; import org.jaudiotagger.audio.asf.data.GUID; import org.jaudiotagger.audio.asf.util.Utils; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.util.*; import java.util.logging.Logger; /** * This class represents a reader implementation, which is able to read ASF * objects (chunks) which store other objects (chunks) within them.<br> * * @author Christian Laireiter * @param <ChunkType> * The {@link ChunkContainer} instance, the implementation will * create. */ abstract class ChunkContainerReader<ChunkType extends ChunkContainer> implements ChunkReader { /** * Logger */ protected static final Logger LOGGER = Logger .getLogger("org.jaudiotabgger.audio"); //$NON-NLS-1$ /** * Within this range, a {@link ChunkReader} should be aware if it fails. */ public final static int READ_LIMIT = 8192; /** * If <code>true</code> each chunk type will only be read once.<br> */ protected final boolean eachChunkOnce; /** * If <code>true</code> due to a {@linkplain #register(Class) registered} * chunk reader, all {@link InputStream} objects passed to * {@link #read(GUID, InputStream, long)} must support mark/reset. */ protected boolean hasFailingReaders = false; /** * Registers GUIDs to their reader classes.<br> */ protected final Map<GUID, ChunkReader> readerMap = new HashMap<GUID, ChunkReader>(); /** * Creates a reader instance, which only utilizes the given list of chunk * readers.<br> * * @param toRegister * List of {@link ChunkReader} class instances, which are to be * utilized by the instance. * @param readChunkOnce * if <code>true</code>, each chunk type (identified by chunk * GUID) will handled only once, if a reader is available, other * chunks will be discarded. */ protected ChunkContainerReader( final List<Class<? extends ChunkReader>> toRegister, final boolean readChunkOnce) { this.eachChunkOnce = readChunkOnce; for (final Class<? extends ChunkReader> curr : toRegister) { register(curr); } } /** * Checks for the constraints of this class. * * @param stream * stream to test. * @throws IllegalArgumentException * If stream does not meet the requirements. */ protected void checkStream(final InputStream stream) throws IllegalArgumentException { if (this.hasFailingReaders && !stream.markSupported()) { throw new IllegalArgumentException( "Stream has to support mark/reset."); } } /** * This method is called by {@link #read(GUID, InputStream, long)} in order * to create the resulting object. Implementations of this class should now * return a new instance of their implementation specific result <b>AND</b> * all data should be read, until the list of chunks starts. (The * {@link ChunkContainer#getChunkEnd()} must return a sane result, too)<br> * * @param streamPosition * position of the stream, the chunk starts. * @param chunkLength * the length of the chunk (from chunk header) * @param stream * to read the implementation specific information. * @return instance of the implementations result. * @throws IOException * On I/O Errors and Invalid data. */ abstract protected ChunkType createContainer(long streamPosition, BigInteger chunkLength, InputStream stream) throws IOException; /** * Gets a configured {@linkplain ChunkReader reader} instance for ASF * objects (chunks) with the specified <code>guid</code>. * * @param guid * GUID which identifies the chunk to be read. * @return an appropriate reader implementation, <code>null</code> if not * {@linkplain #register(Class) registered}. */ protected ChunkReader getReader(final GUID guid) { return this.readerMap.get(guid); } /** * Tests whether {@link #getReader(GUID)} won'timer return <code>null</code>.<br> * * @param guid * GUID which identifies the chunk to be read. * @return <code>true</code> if a reader is available. */ protected boolean isReaderAvailable(final GUID guid) { return this.readerMap.containsKey(guid); } /** * This Method implements the reading of a chunk container.<br> * * @param guid * GUID of the currently read container. * @param stream * Stream which contains the chunk container. * @param chunkStart * The start of the chunk container from stream start.<br> * For direct file streams one can assume <code>0</code> here. * @return <code>null</code> if no valid data found, else a Wrapper * containing all supported data. * @throws IOException * Read errors. * @throws IllegalArgumentException * If one used {@link ChunkReader} could * {@linkplain ChunkReader#canFail() fail} and the stream source * doesn'timer support mark/reset. */ public ChunkType read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException, IllegalArgumentException { checkStream(stream); final CountingInputStream cis = new CountingInputStream(stream); if (!Arrays.asList(getApplyingIds()).contains(guid)) { throw new IllegalArgumentException( "provided GUID is not supported by this reader."); } // For Know the file pointer pointed to an ASF header chunk. final BigInteger chunkLen = Utils.readBig64(cis); /* * now read implementation specific information until the chunk * collection starts and create the resulting object. */ final ChunkType result = createContainer(chunkStart, chunkLen, cis); // 16 bytes have already been for providing the GUID long currentPosition = chunkStart + cis.getReadCount() + 16; final HashSet<GUID> alreadyRead = new HashSet<GUID>(); /* * Now reading header of chuncks. */ while (currentPosition < result.getChunkEnd()) { final GUID currentGUID = Utils.readGUID(cis); final boolean skip = this.eachChunkOnce && (!isReaderAvailable(currentGUID) || !alreadyRead .add(currentGUID)); Chunk chunk; /* * If one reader tells it could fail (new method), then check the * input stream for mark/reset. And use it if failed. */ if (!skip && isReaderAvailable(currentGUID)) { final ChunkReader reader = getReader(currentGUID); if (reader.canFail()) { cis.mark(READ_LIMIT); } chunk = getReader(currentGUID).read(currentGUID, cis, currentPosition); } else { chunk = ChunkHeaderReader.getInstance().read(currentGUID, cis, currentPosition); } if (chunk == null) { /* * Reader failed */ cis.reset(); } else { if (!skip) { result.addChunk(chunk); } currentPosition = chunk.getChunkEnd(); // Always take into account, that 16 bytes have been read prior // to calling this method assert cis.getReadCount() + chunkStart + 16 == currentPosition; } } return result; } /** * Registers the given reader.<br> * * @param <T> * The actual reader implementation. * * @param toRegister * chunk reader which is to be registered. */ private <T extends ChunkReader> void register(final Class<T> toRegister) { try { final T reader = toRegister.newInstance(); for (final GUID curr : reader.getApplyingIds()) { this.readerMap.put(curr, reader); } } catch (InstantiationException e) { LOGGER.severe(e.getMessage()); } catch (IllegalAccessException e) { LOGGER.severe(e.getMessage()); } } }