/* * Copyright (c) 2014 Red Hat, Inc. * * 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.ovirt.engine.api.restapi.xml; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; 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.ValidationEventHandler; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.ovirt.engine.api.model.Api; import org.ovirt.engine.api.model.ObjectFactory; import org.ovirt.engine.api.restapi.invocation.CurrentManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is responsible for converting XML documents into model objects, and the other way around. Note that it * can't be a generic class because if it is then the JAX-RS framework will select other builtin classes that are more * specific. */ @Provider @Consumes(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML) public class JAXBProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object> { /** * The version of the API supported by this provider. */ private static final String SUPPORTED_VERSION = "4"; /** * The logger used by this class. */ private static final Logger log = LoggerFactory.getLogger(JAXBProvider.class); /** * The package of the classes that this provider supports. */ private static final Package typesPackage = Api.class.getPackage(); /** * The factory used to create JAXB elements. */ private ObjectFactory objectFactory = new ObjectFactory(); /** * A index used to speed up finding the factory method used to create JAXB elements. */ private Map<Class<?>, Method> factoryMethods = new HashMap<>(); /** * The factory used to create XML document readers. */ private XMLInputFactory parserFactory; /** * The JAXB jaxbContext used to convert XML documents into the corresponding model objects. */ private JAXBContext jaxbContext; /** * Default event handler recognizes XML parsing as error and not as warning. */ private ValidationEventHandler jaxbHandler = new JAXBValidationEventHandler(); public JAXBProvider() { // In order to create the JAXB element that wraps the object we need to call the method of the object factory // that uses the correct element name, and in order to avoid doing this with every request we populate this // map in advance: for (Method factoryMethod : ObjectFactory.class.getDeclaredMethods()) { Class<?>[] parameterTypes = factoryMethod.getParameterTypes(); if (parameterTypes.length == 1) { factoryMethods.put(parameterTypes[0], factoryMethod); } } // Create a factory that will produce XML parsers that ignore entity references and DTDs: parserFactory = XMLInputFactory.newFactory(); parserFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); parserFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); parserFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // Create a JAXB context for the tyeps package: try { jaxbContext = JAXBContext.newInstance(typesPackage.getName()); } catch (JAXBException exception) { log.error("Can't create JAXB context for package \"{}\"", typesPackage.getName(), exception); } } /** * {@inheritDoc} */ @Override public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return SUPPORTED_VERSION.equals(CurrentManager.get().getVersion()); } /** * {@inheritDoc} */ @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return SUPPORTED_VERSION.equals(CurrentManager.get().getVersion()); } /** * {@inheritDoc} */ @Override public long getSize(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } /** * {@inheritDoc} */ @Override public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { XMLStreamReader reader = null; try { reader = parserFactory.createXMLStreamReader(entityStream, "UTF-8"); return readFrom(reader); } catch (XMLStreamException exception) { throw new IOException(exception); } finally { if (reader != null) { try { reader.close(); } catch (XMLStreamException exception) { log.warn("Can't close XML stream reader.", exception); } } } } /** * Read the XML document using the given reader and convert it to an object. The given reader will be closed by the * caller. */ private Object readFrom(XMLStreamReader reader) throws IOException { try { Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); unmarshaller.setEventHandler(jaxbHandler); Object result = unmarshaller.unmarshal(reader); if (result instanceof JAXBElement) { result = ((JAXBElement) result).getValue(); } return result; } catch (JAXBException exception) { Throwable linked = exception.getLinkedException(); if (linked != null) { Throwable cause = linked; while (cause.getCause() != null) { cause = cause.getCause(); } throw new IOException(cause); } throw new IOException(exception); } } @Override public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { // Find the factory method used to create the JAXB element with the right tag: Method factoryMethod = factoryMethods.get(type); if (factoryMethod == null) { throw new IOException("Can't find factory method for type \"" + type.getName() + "\"."); } // Invoke the method to create the JAXB element: JAXBElement<Object> element; try { element = (JAXBElement<Object>) factoryMethod.invoke(objectFactory, object); } catch (IllegalAccessException|InvocationTargetException exception) { throw new IOException("Error invoking factory method for type \"" + type.getName() + "\".", exception); } // Marshal the element: try { Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.marshal(element, entityStream); } catch (JAXBException exception) { throw new IOException("Can't marshall JAXB element of type \"" + type.getName() + "\".", exception); } } }