/****************************************************************************
* Copyright (C) 2012-2015 ecsec GmbH.
* All rights reserved.
* Contact: ecsec GmbH (info@ecsec.de)
*
* This file is part of the Open eCard App.
*
* GNU General Public License Usage
* This file may be used under the terms of the GNU General Public
* License version 3.0 as published by the Free Software Foundation
* and appearing in the file LICENSE.GPL included in the packaging of
* this file. Please review the following information to ensure the
* GNU General Public License version 3.0 requirements will be met:
* http://www.gnu.org/copyleft/gpl.html.
*
* Other Usage
* Alternatively, this file may be used in accordance with the terms
* and conditions contained in a signed written agreement between
* you and ecsec GmbH.
*
***************************************************************************/
package org.openecard.ws.jaxb;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import javax.annotation.Nonnull;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.bind.annotation.XmlType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wrapper for JAXB marshaller and unmarshaller capable of modifying the supported JAXB types on the fly.
*
* @author Tobias Wich
*/
public class MarshallerImpl {
private static final Logger logger = LoggerFactory.getLogger(MarshallerImpl.class);
private static final ArrayList<Class<?>> baseXmlElementClasses;
private static final FutureTask<JAXBContext> baseJaxbContext;
private static final HashMap<String, JAXBContext> specificContexts;
private boolean userOverride;
private final TreeSet<Class<?>> userClasses;
private Marshaller marshaller;
private Unmarshaller unmarshaller;
static {
// load predefined classes
final Class<?>[] jaxbClasses = getJaxbClasses();
baseXmlElementClasses = new ArrayList<>(jaxbClasses.length);
baseXmlElementClasses.addAll(Arrays.asList(jaxbClasses));
baseJaxbContext = new FutureTask<>(new Callable<JAXBContext>() {
@Override
public JAXBContext call() throws Exception {
try {
return JAXBContext.newInstance(jaxbClasses);
} catch (JAXBException ex) {
logger.error("Failed to create JAXBContext instance.", ex);
throw new RuntimeException("Failed to create JAXBContext.");
}
}
});
new Thread(baseJaxbContext, "JAXB-Classload").start();
specificContexts = new HashMap<>();
}
/**
* Creates a MarshallerImpl instance based on the JAXB types specified in the classpath resource classes.lst.
*/
public MarshallerImpl() {
userOverride = false;
userClasses = new TreeSet<>(new ClassComparator());
}
/**
* Adds the specified JAXB element types class to the list of supported JAXB types.
* This method triggers a recreation of the wrapped marshaller and unmarshaller.
*
* @param c Class of the JAXB element type.
*/
public synchronized void addXmlClass(Class<?> c) {
addBaseClasses();
if (! userClasses.contains(c)) {
//addJaxbClasses(c);
userClasses.add(c);
// class added to set
userOverride = true;
resetMarshaller();
}
}
private void addBaseClasses() {
// add all base classes if the user did not delete them before
if (! userOverride) {
userClasses.addAll(baseXmlElementClasses);
}
}
/**
* Remove all JAXB element types from this instance.
* New types must be added first before this instance is usable for marshalling and unmarshalling again.
*/
public synchronized void removeAllClasses() {
userOverride = true;
userClasses.clear();
resetMarshaller();
}
/**
* Gets the wrapped JAXB marshaller instance.
*
* @return The wrapped JAXB marshaller instance.
* @throws JAXBException If the marshaller could not be created.
*/
public Marshaller getMarshaller() throws JAXBException {
if (marshaller == null) {
loadInstances();
}
return marshaller;
}
/**
* Gets the wrapped JAXB unmarshaller instance.
*
* @return The wrapped JAXB unmarshaller instance.
* @throws JAXBException If the unmarshaller could not be created.
*/
public Unmarshaller getUnmarshaller() throws JAXBException {
if (unmarshaller == null) {
loadInstances();
}
return unmarshaller;
}
private void resetMarshaller() {
marshaller = null;
unmarshaller = null;
}
private synchronized void loadInstances() throws JAXBException {
JAXBContext jaxbCtx;
if (userOverride) {
String classHash = calculateClassesHash();
synchronized (specificContexts) {
if (! specificContexts.containsKey(classHash)) {
jaxbCtx = JAXBContext.newInstance(userClasses.toArray(new Class<?>[userClasses.size()]));
specificContexts.put(classHash, jaxbCtx);
} else {
jaxbCtx = specificContexts.get(classHash);
}
}
} else {
try {
jaxbCtx = baseJaxbContext.get();
} catch (ExecutionException ex) {
logger.error("Failed to create JAXBContext instance.", ex);
throw new RuntimeException("Failed to create JAXBContext.");
} catch (InterruptedException ex) {
logger.error("Thread terminated waiting for the JAXBContext to be created..", ex);
throw new RuntimeException("Thread interrupted during waiting on the creation of the JAXBContext.");
}
}
marshaller = jaxbCtx.createMarshaller();
unmarshaller = jaxbCtx.createUnmarshaller();
}
private static Class<?>[] getJaxbClasses() {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
List<Class<?>> classes = new LinkedList<>();
InputStream classListStream = cl.getResourceAsStream("classes.lst");
InputStream classListStreamC = cl.getResourceAsStream("/classes.lst");
try {
if (classListStream == null && classListStreamC == null) {
throw new IOException("Failed to load classes.lst.");
} else {
// select the one stream that is set
classListStream = (classListStream != null) ? classListStream : classListStreamC;
LineNumberReader r = new LineNumberReader(new InputStreamReader(classListStream));
String next;
// read all entries from file
while ((next = r.readLine()) != null) {
try {
// load class and see if it is a JAXB class
Class<?> c = cl.loadClass(next);
if (isJaxbClass(c)) {
classes.add(c);
}
} catch (ClassNotFoundException ex) {
logger.error("Failed to load class: " + next, ex);
}
}
}
} catch (IOException ex) {
logger.error("Failed to read classes from file classes.lst.", ex);
}
return classes.toArray(new Class<?>[classes.size()]);
}
private static boolean isJaxbClass(Class<?> c) {
return c.isAnnotationPresent(XmlType.class) ||
c.isAnnotationPresent(XmlRegistry.class);
}
private String calculateClassesHash() {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
for (Class<?> c : userClasses) {
md.update(c.getName().getBytes());
}
byte[] digest = md.digest();
return toHexString(digest);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException("MD5 hash algorithm is not supported on your platform.", ex);
}
}
private static String toHexString(@Nonnull byte[] bytes) {
StringWriter writer = new StringWriter(bytes.length * 2);
PrintWriter out = new PrintWriter(writer);
for (int i = 1; i <= bytes.length; i++) {
out.printf("%02X", bytes[i - 1]);
}
return writer.toString();
}
}