/**
* Copyright 2015 Nortal 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 com.nortal.jroad.endpoint;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.activation.DataHandler;
import javax.annotation.Resource;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.attachment.AttachmentMarshaller;
import javax.xml.bind.attachment.AttachmentUnmarshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import com.nortal.jroad.enums.XRoadProtocolVersion;
import com.nortal.jroad.model.BeanXTeeMessage;
import com.nortal.jroad.model.XTeeAttachment;
import com.nortal.jroad.model.XTeeMessage;
import com.nortal.jroad.util.AttachmentUtil;
import com.nortal.jroad.util.SOAPUtil;
/**
* X-Tee endpoint that provides request/response manipulation using Java objects via JAXB API. All extension classes
* must implement the method method {@link AbstractXTeeJAXBEndpoint#invokeBean(T requestBean)}.
*
* @author Dmitri Danilkin
* @author Lauri Lättemäe (lauri.lattemae@nortal.com) - protocol 4.0
*/
public abstract class AbstractXTeeJAXBEndpoint<T> extends AbstractXTeeBaseEndpoint {
private static final class JaxbContextKey {
private final String contextPath;
private final ClassLoader classLoader;
public JaxbContextKey(final String contextPath, final ClassLoader classLoader) {
this.contextPath = contextPath;
this.classLoader = classLoader;
}
@Override
public boolean equals(final Object other) {
if (this == other) {
return true;
}
if (other == null || !getClass().equals(other.getClass())) {
return false;
}
JaxbContextKey otherKey = (JaxbContextKey) other;
return ((contextPath == null) ? (otherKey.contextPath == null) : (contextPath.equals(otherKey.contextPath)))
&& ((classLoader == null) ? (otherKey.classLoader == null) : (classLoader.equals(otherKey.classLoader)));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((classLoader == null) ? 0 : classLoader.hashCode());
result = prime * result + ((contextPath == null) ? 0 : contextPath.hashCode());
return result;
}
}
private static final HashMap<JaxbContextKey, JAXBContext> jaxbContexts = new HashMap<JaxbContextKey, JAXBContext>();
@Resource(name = "contextPath")
private String contextPath;
private ClassLoader jaxbClassLoader;
/**
* Sets the JAXB context package path.
*
* @param pPath A colon separated path of package names where to look for "jaxb.properties" files. The package names
* must match the generated classes which you are going to be used in the application.
*/
public void setContextPath(final String pPath) {
this.contextPath = pPath;
}
public ClassLoader getJaxbClassLoader() {
return jaxbClassLoader;
}
/** Sets the classloader used by <code>JAXBContext</code>. Usually this is safe left unspecified. */
public void setJaxbClassLoader(final ClassLoader jaxbClassLoader) {
this.jaxbClassLoader = jaxbClassLoader;
}
private Class<T> paringKehaClass;
public void setParingKehaClass(final Class<T> paringKehaClass) {
this.paringKehaClass = paringKehaClass;
}
protected Class<T> getParingKehaClass() {
return paringKehaClass;
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
protected void invokeInternal(final XTeeMessage<Document> request, final XTeeMessage<Element> response)
throws Exception {
if (getParingKehaClass() == null) {
throw new IllegalStateException("Query body class ('requestClass') is unset/unspecified!");
}
JAXBContext requestJc = getJAXBContextInstance();
Unmarshaller requestUnmarshaller = requestJc.createUnmarshaller();
requestUnmarshaller.setAttachmentUnmarshaller(new XTeeAttachmentUnmarshaller(request));
Document requestOnly = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
if (XRoadProtocolVersion.V2_0 == version) {
requestOnly.appendChild(requestOnly.importNode((Node) XPathFactory.newInstance().newXPath().evaluate("//*[local-name()='keha']",
request.getContent(),
XPathConstants.NODE),
true));
} else {
requestOnly.appendChild(requestOnly.importNode(SOAPUtil.getFirstNonTextChild(request.getContent()), true));
}
XTeeMessage<T> jaxbRequestMessage = new BeanXTeeMessage<T>(request.getHeader(),
requestUnmarshaller.unmarshal(requestOnly.getDocumentElement(),
getParingKehaClass()).getValue(),
request.getAttachments());
XTeeMessage<Object> jaxbResponseMessage =
new BeanXTeeMessage<Object>(response.getHeader(), null, new ArrayList<XTeeAttachment>());
invoke(jaxbRequestMessage, jaxbResponseMessage);
Object bean = jaxbResponseMessage.getContent();
if (bean != null) { // If you do not need to send an object as response, <keha /> is sufficient.
Node parent = response.getContent().getParentNode();
Node child = parent.removeChild(response.getContent());
JAXBContext responseJc = getJAXBContextInstance();
Marshaller responseMarshaller = responseJc.createMarshaller();
responseMarshaller.setAttachmentMarshaller(new XTeeAttachmentMarshaller(response));
// TODO Lauri: some namespace hacking might be needed if existing service schema is changed according to new
// standard while upgrading. J-road clients do not mind tho :)
if (XRoadProtocolVersion.V2_0 == version) {
responseMarshaller.marshal(new JAXBElement(new QName("keha"), bean.getClass(), bean), parent);
} else {
responseMarshaller.marshal(new JAXBElement(new QName(response.getContent().getNamespaceURI(),
child.getLocalName(),
response.getContent().getPrefix()),
bean.getClass(),
bean),
parent);
}
}
}
private JAXBContext getJAXBContextInstance() throws JAXBException {
synchronized (AbstractXTeeJAXBEndpoint.jaxbContexts) {
JaxbContextKey key = new JaxbContextKey(contextPath, jaxbClassLoader);
JAXBContext ctx = AbstractXTeeJAXBEndpoint.jaxbContexts.get(key);
if (ctx == null) {
ctx = (jaxbClassLoader == null)
? JAXBContext.newInstance(contextPath)
: JAXBContext.newInstance(contextPath, jaxbClassLoader);
AbstractXTeeJAXBEndpoint.jaxbContexts.put(key, ctx);
}
return ctx;
}
}
private static class XTeeAttachmentUnmarshaller extends AttachmentUnmarshaller {
private final Map<String, XTeeAttachment> cidMap = new HashMap<String, XTeeAttachment>();
XTeeAttachmentUnmarshaller(final XTeeMessage<?> message) {
for (XTeeAttachment attachment : message.getAttachments()) {
cidMap.put(attachment.getCid(), attachment);
}
}
private XTeeAttachment getAttachment(String contentId) {
if (contentId.startsWith("cid:")) {
contentId = contentId.substring("cid:".length());
try {
contentId = URLDecoder.decode(contentId, "UTF-8");
} catch (UnsupportedEncodingException e) {
// ignore, because data was probably not UTF-8
}
contentId = '<' + contentId + '>';
}
return cidMap.get(contentId);
}
@Override
public byte[] getAttachmentAsByteArray(final String contentId) {
try {
return getAttachment(contentId).getData();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public DataHandler getAttachmentAsDataHandler(final String cid) {
XTeeAttachment attachment = getAttachment(cid);
return attachment.getDataHandler();
}
// Currently MTOM will be disabled
@Override
public boolean isXOPPackage() {
return false;
}
}
private static class XTeeAttachmentMarshaller extends AttachmentMarshaller {
private final List<XTeeAttachment> attachments;
private long salt = 0;
public XTeeAttachmentMarshaller(final XTeeMessage<?> message) {
this.attachments = message.getAttachments();
}
@Override
public String addMtomAttachment(final byte[] arg0,
final int arg1,
final int arg2,
final String arg3,
final String arg4,
final String arg5) {
throw new UnsupportedOperationException("MTOM Support is disabled!");
}
@Override
public String addMtomAttachment(final DataHandler arg0, final String arg1, final String arg2) {
throw new UnsupportedOperationException("MTOM Support is disabled!");
}
@Override
public String addSwaRefAttachment(final DataHandler handler) {
salt++;
String contentId = AttachmentUtil.getUniqueCid();
attachments.add(new XTeeAttachment(contentId, handler));
return "cid:" + contentId;
}
// Currently MTOM will be disabled
@Override
public boolean isXOPPackage() {
return false;
}
}
protected void invoke(final XTeeMessage<T> request, final XTeeMessage<Object> response) throws Exception {
response.setContent(invokeBean(request.getContent()));
}
protected Object invokeBean(final T requestBean) throws IOException {
throw new IllegalStateException("You must override either the 'invokeBean' or the 'invoke' method!");
}
}