/*******************************************************************************
* Copyright (c) 2013 Jens Kristian Villadsen.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* Jens Kristian Villadsen - Lead developer, owner and creator
******************************************************************************/
/*
* Digital Audio Access Protocol (DAAP) Library
* Copyright (C) 2004-2010 Roger Kapsi
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dyndns.jkiddo.dmp;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import org.dyndns.jkiddo.dmap.chunks.audio.SongArtist;
import org.dyndns.jkiddo.dmp.chunks.ByteChunk;
import org.dyndns.jkiddo.dmp.chunks.Chunk;
import org.dyndns.jkiddo.dmp.chunks.ChunkFactory;
import org.dyndns.jkiddo.dmp.chunks.ContainerChunk;
import org.dyndns.jkiddo.dmp.chunks.DateChunk;
import org.dyndns.jkiddo.dmp.chunks.IntChunk;
import org.dyndns.jkiddo.dmp.chunks.LongChunk;
import org.dyndns.jkiddo.dmp.chunks.RawChunk;
import org.dyndns.jkiddo.dmp.chunks.ShortChunk;
import org.dyndns.jkiddo.dmp.chunks.StringChunk;
import org.dyndns.jkiddo.dmp.chunks.VersionChunk;
import org.dyndns.jkiddo.dmp.chunks.media.ListingItem;
import org.dyndns.jkiddo.dmp.util.DmapUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DmapInputStream extends BufferedInputStream
{
private static final Logger LOGGER = LoggerFactory.getLogger(DmapInputStream.class);
private ChunkFactory factory;
private final boolean specialCaseProtocolViolation;
private int contentLength;
public DmapInputStream(final InputStream in)
{
//super(in);
this(in, false);
//this.specialCaseProtocolViolation = false;
}
public DmapInputStream(final InputStream in, final boolean specialCaseProtocolViolation)
{
super(in);
this.specialCaseProtocolViolation = specialCaseProtocolViolation;
}
private int getChunkContentLength()
{
return contentLength;
}
@Override
public int read() throws IOException
{
final int b = super.read();
if(b < 0)
{
throw new EOFException();
}
return b;
}
/*
* Re: skip(length-Chunk.XYZ_LENGTH); iTunes states in Content-Codes responses that Chunk X is of type Y and has hence the length Z. A Byte has for example the length 1. But in some cases iTunes uses a different length for Bytes! It's probably a bug in iTunes...
*/
private int readByte(final int length) throws IOException
{
skip(length - Chunk.BYTE_LENGTH);
return read();
}
private int readShort(final int length) throws IOException
{
skip(length - Chunk.SHORT_LENGTH);
return (read() << 8) | read();
}
private long readDate(final int length) throws IOException
{
skip(length - Chunk.INT_LENGTH);
long v = read();
v = v << 24;
long v2,v3,v4;
v2 = read() << 16;
v3 = read() << 8;
v4 = read();
v = v | v2 | v3 | v4;
return v;
}
private int readInt(final int length) throws IOException
{
skip(length - Chunk.INT_LENGTH);
return (read() << 24) | (read() << 16) | (read() << 8) | read();
}
private long readLong(final int length) throws IOException
{
skip(length - Chunk.LONG_LENGTH);
return (read() << 54l) | (read() << 48l) | (read() << 40l)
| (read() << 32l) | (read() << 24l) | (read() << 16l)
| (read() << 8l) | read();
}
private String readString(final int length) throws IOException
{
if(length == 0)
{
return null;
}
final byte[] b = new byte[length];
read(b, 0, b.length);
return new String(b, DmapUtil.UTF_8);
}
/*private int readContentCode() throws IOException
{
return readInt(Chunk.INT_LENGTH);
}*/
private String readContentCode() throws IOException
{
return readString(Chunk.INT_LENGTH);
}
private int readLength() throws IOException
{
return readInt(Chunk.INT_LENGTH);
}
@SuppressWarnings("unchecked")
public <T> T getChunk(final Class<T> clazz) throws IOException, ProtocolViolationException
{
return (T) getChunk();
}
@SuppressWarnings("unchecked")
public <T extends Chunk> T getChunk() throws IOException, ProtocolViolationException
{
final String contentCode = readContentCode();
contentLength = readLength();
if(factory == null)
{
factory = new ChunkFactory();
}
T chunk = factory.newChunk(contentCode);
if (specialCaseProtocolViolation && chunk.getClass().equals(ListingItem.class))
{
chunk = (T) new SongArtist();
}
if(contentLength > 0)
{
if(chunk instanceof ByteChunk)
{
checkLength(chunk, Chunk.BYTE_LENGTH, contentLength);
((ByteChunk) chunk).setValue(readByte(contentLength));
}
else if(chunk instanceof ShortChunk)
{
checkLength(chunk, Chunk.SHORT_LENGTH, contentLength);
((ShortChunk) chunk).setValue(readShort(contentLength));
}
else if(chunk instanceof IntChunk)
{
checkLength(chunk, Chunk.INT_LENGTH, contentLength);
((IntChunk) chunk).setValue(readInt(contentLength));
}
else if(chunk instanceof LongChunk)
{
checkLength(chunk, Chunk.LONG_LENGTH, contentLength);
((LongChunk) chunk).setValue(readLong(contentLength));
}
else if(chunk instanceof StringChunk)
{
((StringChunk) chunk).setValue(readString(contentLength));
}
else if(chunk instanceof DateChunk)
{
checkLength(chunk, Chunk.DATE_LENGTH, contentLength);
((DateChunk) chunk).setValue(readDate(contentLength));
}
else if(chunk instanceof VersionChunk)
{
checkLength(chunk, Chunk.VERSION_LENGTH, contentLength);
((VersionChunk) chunk).setValue(readInt(contentLength));
}
else if(chunk instanceof RawChunk)
{
final byte[] b = new byte[contentLength];
read(b, 0, b.length);
((RawChunk) chunk).setValue(b);
}
else if(chunk instanceof ContainerChunk)
{
final byte[] b = new byte[contentLength];
read(b, 0, b.length);
final DmapInputStream in = new DmapInputStream(new ByteArrayInputStream(b), this.specialCaseProtocolViolation);
while(in.available() > 0)
{
try
{
((ContainerChunk) chunk).add(in.getChunk());
}
catch(final ProtocolViolationException pve)
{
LOGGER.warn(pve.getMessage(), pve);
in.skip(in.getChunkContentLength());
}
//
// Chunk newChunk = in.getChunk();
// if(newChunk != null)
// ((ContainerChunk) chunk).add(newChunk);
}
in.close();
}
else
{
throw new IOException("Unknown Chunk Type: " + chunk);
}
}
return chunk;
}
/**
* Throws an IOE if expected differs from length
*/
private static void checkLength(final Chunk chunk, final int expected, final int length)
{
if (expected != length && LOGGER.isWarnEnabled())
{
// throw new IOException("Expected a chunk with length " + expected
// + " but got " + length + " (" + chunk.getContentCodeString() +
// ")");
LOGGER.warn("Expected a chunk with length " + expected + " but got " + length + " (" + chunk.getContentCodeString() + ") " + chunk.getClass());
}
}
}