
net.yapbam.data.xml.AbstractSerializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of yapbam-commons Show documentation
Show all versions of yapbam-commons Show documentation
Commons Yapbam classes used by desktop and Android versions.
package net.yapbam.data.xml;
import java.io.*;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import net.yapbam.data.*;
import net.yapbam.data.xml.task.DecrypterTask;
import net.yapbam.data.xml.task.DeflaterTask;
import net.yapbam.data.xml.task.EncrypterTask;
import net.yapbam.data.xml.task.InflaterTask;
import net.yapbam.data.xml.task.PipeTask;
import net.yapbam.util.Crypto;
/** The class implements xml yapbam data serialization and deserialization to (or from) an URL.
* Currently supported URL type are :
* - file.
*
*/
public abstract class AbstractSerializer {
private static final boolean NEW_ENCODER_ON = true;
/** The password encoded file header scheme.
* the * characters means "the ending version is coded there".
*/
private static final byte[] PASSWORD_ENCODED_FILE_HEADER = toBytes(""); //$NON-NLS-1$
private static final String V1 = "1.0"; //$NON-NLS-1$
private static final String V2 = "2.0"; //$NON-NLS-1$
static {
// A lot of code relies on the fact that V1 and V2 have the same length and that this length is the same as the number
// of * in PASSWORD_ENCODED_FILE_HEADER
// This code verifies it is always true
int nb = 0;
for (byte c : PASSWORD_ENCODED_FILE_HEADER) {
if (c=='*') {
nb++;
}
}
try {
if ((V1.getBytes(Crypto.UTF8).length!=nb) || (V2.getBytes(Crypto.UTF8).length!=nb) || (nb==0)) {
throw new IllegalArgumentException("Encoded file headers versions have invalid lengths !"); //$NON-NLS-1$
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static byte[] toBytes(String magicString) {
byte[] bytes;
try {
bytes = magicString.getBytes(Crypto.UTF8);
} catch (UnsupportedEncodingException e) {
bytes = null;
}
return bytes;
}
/** Saves the data to a stream.
* @param data The data to save
* @param out The outputStream (Note that this stream is not closed by this method).
* @param password the password used to protect written data
* @param report a progress report
* @throws IOException if something goes wrong while writing
*/
public void write(final T data, OutputStream out, String password, final ProgressReport report) throws IOException {
if (password!=null) {
// If the file has to be protected by a password
// outputs the magic bytes that will allow Yapbam to recognize the file is crypted.
out.write(getHeader(NEW_ENCODER_ON?V2:V1));
final PipedOutputStream xmlOutput = new PipedOutputStream();
PipedInputStream compressorInput = new PipedInputStream(xmlOutput);
PipedOutputStream compressorOutput = new PipedOutputStream();
PipedInputStream encoderInput = new PipedInputStream(compressorOutput);
ExecutorService service = new ThreadPoolExecutor(0, Integer.MAX_VALUE,0, TimeUnit.SECONDS, new SynchronousQueue());
List> futures = new ArrayList>(3);
Callable c = new Callable() {
@Override
public Void call() throws Exception {
try {
directWrite(data, xmlOutput, report);
return null;
} finally {
xmlOutput.close();
}
}
};
futures.add(service.submit(c));
futures.add(service.submit(new DeflaterTask(compressorInput, compressorOutput)));
// As encryterTask closes its output stream (required to process the doFinal of the encryption cipher),
// We can't pass it directly the out stream. So we will add an intermediate stream
PipedOutputStream encrypterOutput = new PipedOutputStream();
PipedInputStream entryWriterInput = new PipedInputStream(encrypterOutput);
futures.add(service.submit(new EncrypterTask(encoderInput, encrypterOutput, password, !NEW_ENCODER_ON)));
futures.add(service.submit(new PipeTask(entryWriterInput, out)));
try {
// Wait encoding is ended and gets the errors
for (Future extends Object> future : futures) {
future.get();
}
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException)cause;
} else {
throw new RuntimeException(cause);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
directWrite(data, out, report);
}
}
public abstract void directWrite(T data, OutputStream out, ProgressReport report) throws IOException;
private static byte[] getHeader(String version) {
int index = 0;
byte[] result = new byte[PASSWORD_ENCODED_FILE_HEADER.length];
for (int i = 0; i < result.length; i++) {
if (PASSWORD_ENCODED_FILE_HEADER[i]!='*') {
result[i] = PASSWORD_ENCODED_FILE_HEADER[i];
} else {
result[i] = (byte) version.charAt(index);
index++;
}
}
return result;
}
/** Reads global data.
* @param password The password of the data (null if the data is not password protected)
* @param in The input stream containing the data (if the stream is a stream on a zipped file, for instance created by write(GlobalData, ZipOutputStream, String, ProgressReport)
* the data is automatically unzipped from the first entry).
* @param report A progress report to observe the progress, or null
* @return The read data.
* @throws IOException If something goes wrong while reading
* @throws AccessControlException If the password is wrong. Note that if data is not password protected, password argument is ignored
* @throws UnsupportedFormatException If the format of data in the input stream is not supported
*/
public T read(final String password, InputStream in, final ProgressReport report) throws IOException, AccessControlException {
SerializationData serializationData = getSerializationData(in);
boolean encoded = serializationData.isPasswordRequired;
if (encoded) {
if (password==null) {
throw new AccessControlException("Stream is encoded but password is null"); //$NON-NLS-1$
}
// Pass the header
for (int i = 0; i < PASSWORD_ENCODED_FILE_HEADER.length; i++) {
in.read();
}
// Read the file content
if (! (serializationData.version.equals(V1) || serializationData.version.equals(V2))) {
throw new UnsupportedFileVersionException("encoded "+serializationData.version);
}
PipedOutputStream decoderOutput = new PipedOutputStream();
PipedInputStream deflaterInput = new PipedInputStream(decoderOutput);
PipedOutputStream deflaterOutput = new PipedOutputStream();
final PipedInputStream readerInput = new PipedInputStream(deflaterOutput);
ExecutorService service = new ThreadPoolExecutor(0, Integer.MAX_VALUE,0, TimeUnit.SECONDS, new SynchronousQueue());
try {
Future decrypter = service.submit(new DecrypterTask(in, decoderOutput, password, serializationData.version.equals(V1)));
Future inflater = service.submit(new InflaterTask(deflaterInput, deflaterOutput));
Callable c = new Callable() {
@Override
public T call() throws Exception {
try {
return directRead(password, readerInput, report);
} finally {
readerInput.close();
}
}
};
Future reader = service.submit(c);
decrypter.get(); // Wait encoding is ended
inflater.get(); // Wait encoding is ended
return reader.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException)cause;
} else if (cause instanceof AccessControlException) {
throw (AccessControlException)cause;
} else {
throw new RuntimeException(cause);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
// Stream is not encoded
return directRead(password, in, report);
}
}
/** Reads the data contained in a stream.
* @param password The stream password
* @param in The input stream where to read data
* @param report A progress report.
* @return The read data, or null if the operation is cancelled (by calling report.cancel() on another thread).
* @throws IOException if something goes wrong
*/
public abstract T directRead(String password, InputStream in, ProgressReport report) throws IOException;
/** Tests whether a password is the right one for an input stream.
* @param in The stream containing Yapbam data
* @param password A password (null for no password)
* @return true if password is ok, false if not.
* Please note that passing a non null password for an non protected stream returns false.
* In order to test if a stream is protected or not, you may call this method with null password. If it returns true,
* the stream is not protected.
* WARNING: This method leaves input stream in an undetermined state (you should close it and reopen it before passing it to read, for instance).
* @throws IOException If an I/O error occurred
*/
public boolean isPasswordOk(InputStream in, String password) throws IOException {
SerializationData serializationData = getSerializationData(in);
if (!serializationData.isPasswordRequired) {
return password==null;
} else {
try {
if (password==null) {
return false;
}
// Pass the header
for (int i = 0; i < PASSWORD_ENCODED_FILE_HEADER.length; i++) {
in.read();
}
DecrypterTask.verifyPassword(in, password);
return true;
} catch (AccessControlException e) {
return false;
}
}
}
// private static void showContent(InputStream in) throws IOException {
// BufferedReader buf = new BufferedReader(new InputStreamReader(in));
// try {
// for (String line=buf.readLine();line!=null;line=buf.readLine()) {
// System.out.println (line);
// }
// System.out.println ("End of stream");
// } finally {
// buf.close();
// }
// }
/** Gets the data about a stream (what is its version, is it encoded or not, etc...).
*
WARNING: This method leaves the stream with a non determinate number of read bytes if it
* does not support mark/reset methods. If it supports these methods (like BufferedInputStream), the stream remains
* unchanged.
* @param in the stream.
* @return A SerializationData instance
* @throws IOException
*/
private static SerializationData getSerializationData(InputStream in) throws IOException {
if (in.markSupported()) {
in.mark(PASSWORD_ENCODED_FILE_HEADER.length);
}
boolean isEncoded = true;
StringBuilder encodingVersion = new StringBuilder();
for (int i = 0; i < PASSWORD_ENCODED_FILE_HEADER.length; i++) {
int c = in.read();
if (PASSWORD_ENCODED_FILE_HEADER[i]=='*') {
encodingVersion.append((char)c);
} else {
if (c!=PASSWORD_ENCODED_FILE_HEADER[i]) {
isEncoded = false;
break;
}
}
}
if (in.markSupported()) {
// Reset the stream (getSerializationData doesn't guarantee the position of the stream)
in.reset();
}
return new SerializationData(isEncoded, encodingVersion.toString());
}
public static class SerializationData {
private boolean isPasswordRequired;
private String version;
private SerializationData(boolean isEncoded, String version) {
this.isPasswordRequired = isEncoded;
this.version = version;
}
public boolean isPasswordRequired() {
return isPasswordRequired;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy