package com.dreikraft.axbo.sound;
import com.dreikraft.axbo.beanutils.EnumConverter;
import com.dreikraft.axbo.crypto.CryptoException;
import com.dreikraft.axbo.crypto.CryptoUtil;
import com.dreikraft.axbo.util.ByteUtil;
import com.dreikraft.axbo.util.FileUtil;
import com.dreikraft.axbo.util.StringUtil;
import com.dreikraft.axbo.util.zip.ZipClosingInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;
import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.ExtendedBaseRules;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
/**
* Utilities for saving, restoring and verifying a Soundpackage file.
*
* @author jan_solo
* @author $Author: illetsch $
* @version $Revision
*/
public class SoundPackageUtil {
/**
* the commons logger category
*/
public static final Log log = LogFactory.getLog(SoundPackageUtil.class);
/**
* blank character {@value}
*/
public static final String SP = " ";
/**
* slash character {@value}
*/
public static final String SL = "/";
/**
* the default encoding for the xml files and streams {@value}
*/
public static final String ENCODING = "UTF-8";
/**
* the buffer size of the file buffer {@value}
*/
public static final int BUF_SIZE = 1024;
public static final String SOUND_DATA_FILE_EXT = ".axs";
public static final String SOUNDS_PATH_PREFIX = "sounds";
public static final String pattern = "dd MM yyyy HH:mm";
public static final DateLocaleConverter dateConverter =
new DateLocaleConverter(Locale.getDefault(), pattern);
public static final int WAV_PREAMBEL_LEN = 0x3A;
public static final String PACKAGE_INFO = "package-info.xml";
private static final String PUBLIC_KEY_HASH =
"E9 A1 51 5A A1 FA 27 AE DA C7 B0 00 9B 86 E9 85";
private static final String PUBLIC_KEY_FILE =
"/resources/sounds.pub.rsa";
private static final String LICENSE_KEY_ENTRY = "license.key";
/**
* Enumeration with all node types of the package-info.xml.
*/
public static enum SoundPackageNodes {
axboSounds, packageName, creator, creationDate, security, serialNumber,
enforced, sounds, sound, displayName, axboFile, path, type
};
/**
* Enumeration with all possible attributes of the package-info.xml.
*/
public static enum SoundPackageAttributes {
id
};
static {
ConvertUtils.register(new EnumConverter(), SoundType.class);
ConvertUtils.register(dateConverter, Date.class);
}
/**
* Reads meta information from package-info.xml (as stream)
*
* @param packageInfoXmlStream the package-info.xml FileInputStream
* @return the sound package info read from the stream
* @throws com.dreikraft.infactory.sound.SoundPackageException encapsulates
* all low level (IO) exceptions
*/
public static SoundPackage readPackageInfo(InputStream packageInfoXmlStream)
throws SoundPackageException {
Digester digester = new Digester();
digester.setValidating(false);
digester.setRules(new ExtendedBaseRules());
digester.addObjectCreate(SoundPackageNodes.axboSounds.toString(),
SoundPackage.class);
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.packageName, "name");
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.creator, "creator");
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.creationDate, "creationDate");
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.security + SL + SoundPackageNodes.serialNumber,
"serialNumber");
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.security + SL + SoundPackageNodes.enforced,
"securityEnforced");
digester.addObjectCreate(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds, ArrayList.class);
digester.addSetNext(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds, "setSounds");
digester.addObjectCreate(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound, Sound.class);
digester.addSetNext(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound, "add");
digester.addSetProperties(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound, "id", "id");
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound + SL
+ SoundPackageNodes.displayName, "name");
digester.addObjectCreate(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound + SL
+ SoundPackageNodes.axboFile, SoundFile.class);
digester.addSetNext(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound + SL
+ SoundPackageNodes.axboFile, "setAxboFile");
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound + SL
+ SoundPackageNodes.axboFile + SL + SoundPackageNodes.path);
digester.addBeanPropertySetter(SoundPackageNodes.axboSounds + SL
+ SoundPackageNodes.sounds + SL + SoundPackageNodes.sound + SL
+ SoundPackageNodes.axboFile + SL + SoundPackageNodes.type);
try {
SoundPackage soundPackage = (SoundPackage) digester.parse(
packageInfoXmlStream);
return soundPackage;
} catch (Exception ex) {
throw new SoundPackageException(ex);
}
}
/**
* retrieves an entry from the package file (ZIP file)
*
* @param packageFile the sound package file
* @param entryName the name of the entry in the ZIP file
* @throws com.dreikraft.infactory.sound.SoundPackageException encapsulates
* all low level (IO) exceptions
* @return the entry data as stream
*/
public static InputStream getPackageEntryStream(File packageFile,
String entryName) throws SoundPackageException {
if (packageFile == null) {
throw new SoundPackageException(new IllegalArgumentException(
"missing package file"));
}
InputStream keyIn = null;
try {
ZipFile packageZip = new ZipFile(packageFile);
// get key from package
ZipEntry keyEntry = packageZip.getEntry(LICENSE_KEY_ENTRY);
keyIn = packageZip.getInputStream(keyEntry);
Key key = CryptoUtil.unwrapKey(keyIn, PUBLIC_KEY_FILE);
// read entry
ZipEntry entry = packageZip.getEntry(entryName);
return new ZipClosingInputStream(packageZip,
CryptoUtil.decryptInput(packageZip.getInputStream(entry), key));
} catch (ZipException ex) {
throw new SoundPackageException(ex);
} catch (IOException ex) {
throw new SoundPackageException(ex);
} catch (CryptoException ex) {
throw new SoundPackageException(ex);
} finally {
try {
if (keyIn != null) {
keyIn.close();
}
} catch (IOException ex) {
log.error(ex.getMessage(), ex);
}
}
}
/**
* Extracts the packageFile and writes its content in temporary directory
*
* @param packageFile the packageFile (zip format)
* @param tempDir the tempDir directory
*/
public static void extractPackage(File packageFile, File tempDir)
throws SoundPackageException {
if (packageFile == null) {
throw new SoundPackageException(new IllegalArgumentException(
"missing package file"));
}
ZipFile packageZip = null;
try {
packageZip = new ZipFile(packageFile);
Enumeration<?> entries = packageZip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
String entryName = entry.getName();
if (log.isDebugEnabled()) {
log.debug("ZipEntry name: " + entryName);
}
if (entry.isDirectory()) {
File dir = new File(tempDir, entryName);
if (dir.mkdirs())
log.info("successfully created dir: " + dir.getAbsolutePath());
} else {
FileUtil.createFileFromInputStream(getPackageEntryStream(packageFile,
entryName),
tempDir + File.separator + entryName);
}
}
} catch (FileNotFoundException ex) {
throw new SoundPackageException(ex);
} catch (IOException ex) {
throw new SoundPackageException(ex);
} finally {
try {
if (packageZip != null)
packageZip.close();
} catch (IOException ex) {
log.error(ex.getMessage(), ex);
}
}
}
/**
* saves a sound package with all meta information and audio files to a ZIP
* file and creates the security tokens.
*
* @param packageFile the zip file, where the soundpackage should be stored
* @param soundPackage the sound package info
* @throws com.dreikraft.infactory.sound.SoundPackageException encapsulates
* all low level (IO) exceptions
*/
public static void exportSoundPackage(final File packageFile,
final SoundPackage soundPackage)
throws SoundPackageException {
if (packageFile == null) {
throw new SoundPackageException(new IllegalArgumentException(
"null package file"));
}
if (packageFile.delete()) {
log.info("successfully deleted file: " + packageFile.getAbsolutePath());
}
ZipOutputStream out = null;
InputStream in = null;
try {
out = new ZipOutputStream(new FileOutputStream(packageFile));
out.setLevel(9);
// write package info
writePackageInfoZipEntry(soundPackage, out);
// create path entries
ZipEntry soundDir = new ZipEntry(SOUNDS_PATH_PREFIX + SL);
out.putNextEntry(soundDir);
out.flush();
out.closeEntry();
// write files
for (Sound sound : soundPackage.getSounds()) {
File axboFile = new File(sound.getAxboFile().getPath());
in = new FileInputStream(axboFile);
writeZipEntry(SOUNDS_PATH_PREFIX + SL + axboFile.getName(),
out, in);
in.close();
}
} catch (FileNotFoundException ex) {
throw new SoundPackageException(ex);
} catch (IOException ex) {
throw new SoundPackageException(ex);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ex) {
log.error("failed to close ZipOutputStream", ex);
}
}
try {
if (in != null)
in.close();
} catch (IOException ex) {
log.error("failed to close FileInputStream", ex);
}
}
}
private static void writePackageInfoZipEntry(final SoundPackage soundPackage,
final ZipOutputStream out) throws
UnsupportedEncodingException,
IOException {
// write xml to temporary byte array, because of stripping problems, when
// directly writing to encrypted stream
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding(ENCODING);
XMLWriter writer = new XMLWriter(bOut, format);
writer.setEscapeText(true);
writer.write(createPackageInfoXml(soundPackage));
writer.close();
// write temporary byte array to encrypet zip entry
ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray());
writeZipEntry(PACKAGE_INFO, out, bIn);
}
private static void writeZipEntry(final String entryName,
final ZipOutputStream out, final InputStream in)
throws IOException {
try {
ZipEntry fileEntry = new ZipEntry(entryName);
out.putNextEntry(fileEntry);
final byte[] buf = new byte[BUF_SIZE];
int avail;
while ((avail = in.read(buf)) != -1) {
out.write(buf, 0, avail);
}
} finally {
out.flush();
out.closeEntry();
}
}
/**
* creates a package-info.xml from the SoundPackage Bean
*
* @param soundPackage a SoundPackage Bean containing all the meta information
* @return a dom4j document
*/
public static Document createPackageInfoXml(final SoundPackage soundPackage) {
Document document = DocumentHelper.createDocument();
document.setXMLEncoding(ENCODING);
Element rootNode =
document.addElement(SoundPackageNodes.axboSounds.toString());
rootNode.addElement(SoundPackageNodes.packageName.toString()).
addText(soundPackage.getName());
rootNode.addElement(SoundPackageNodes.creator.toString()).
addText(soundPackage.getCreator());
rootNode.addElement(SoundPackageNodes.creationDate.toString()).
addText(new SimpleDateFormat(pattern).format(soundPackage.
getCreationDate()));
Element securityNode =
rootNode.addElement(SoundPackageNodes.security.toString());
securityNode.addElement(SoundPackageNodes.serialNumber.toString()).
addText(soundPackage.getSerialNumber());
securityNode.addElement(SoundPackageNodes.enforced.toString()).
addText("" + soundPackage.isSecurityEnforced());
Element soundsNode =
rootNode.addElement(SoundPackageNodes.sounds.toString());
int id = 1;
for (Sound sound : soundPackage.getSounds()) {
Element soundNode =
soundsNode.addElement(SoundPackageNodes.sound.toString());
soundNode.addAttribute(SoundPackageAttributes.id.toString(),
String.valueOf(id));
soundNode.addElement(SoundPackageNodes.displayName.toString()).addText(
sound.getName());
Element axboFileNode = soundNode.addElement(
SoundPackageNodes.axboFile.toString());
axboFileNode.addElement(SoundPackageNodes.path.toString()).setText(
sound.getAxboFile().extractName());
axboFileNode.addElement(SoundPackageNodes.type.toString()).setText(
sound.getAxboFile().getType().toString());
id++;
}
return document;
}
/**
* verifies the security token of the SoundPackage. Verifies that the
* SoundPackage has not been altered
*
* @param packageFile the SoundPackage ZIP file
* @return true if the sound package has not been altered. False if somebody
* changed the contents of the sound package.
*/
public static boolean verifyPackage(File packageFile) {
try {
// check, whether the public key file has not been changed
byte[] pubKeyBytes = CryptoUtil.readKey(PUBLIC_KEY_FILE);
if (!PUBLIC_KEY_HASH.equals(
ByteUtil.dumpByteArray(CryptoUtil.calcMD5(pubKeyBytes)).trim())) {
return false;
}
return true;
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
return false;
}
}
/**
* calculate the size of all audio files in this package that will be uploaded
* to the aXbo clock.
*
* @param soundPackage
* @return
*/
public static long calculateSoundFilesSize(SoundPackage soundPackage) {
int size = 0;
for (Sound sound : soundPackage.getSounds()) {
File f = new File(sound.getAxboFile().getPath());
size += (f.length() - WAV_PREAMBEL_LEN);
}
return size;
}
/**
* Checks if for every {@link Sound} object a name and axbo file was set.
* Return
* <CODE>null</CODE> if all names and files were provided otherwise a
* {@link String} with the bundle key for the error message is returned.
*
* @param sounds <CODE>List</CODE> with {@link Sound} objects
* @return <CODE>null</CODE> if all names and files are set otherwise return
* {@link String} with resource bundle key for the according error message.
*/
public static void validateSoundPackage(SoundPackage soundPackage) throws
SoundPackageException {
// check if soundpackage name was typed in
if (StringUtil.isEmpty(soundPackage.getName())) {
throw new MissingSoundPackageNameException();
}
// check if serial number is empty
if (StringUtil.isEmpty(soundPackage.getSerialNumber())) {
throw new MissingSerialNumberException();
}
// check if every sound is complete
List<Sound> sounds = soundPackage.getSounds();
for (Sound sound : sounds) {
if (StringUtil.isEmpty(sound.getName())) {
throw new MissingSoundNameException();
}
if (sound.getAxboFile() == null) {
throw new MissingSoundFileException();
}
}
}
public static File validateSoundPackageFilename(File f) {
String filename = f.getName();
if (!filename.endsWith(SOUND_DATA_FILE_EXT)) {
return new File(f.getPath() + SOUND_DATA_FILE_EXT);
} else {
return f;
}
}
}