/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.isis.applib.services.jaxb; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import org.apache.isis.applib.ApplicationException; import org.apache.isis.applib.DomainObjectContainer; import org.apache.isis.applib.NonRecoverableException; import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.applib.services.dto.Dto_downloadXsd; public interface JaxbService { @Programmatic Object fromXml(JAXBContext jaxbContext, String xml); @Programmatic Object fromXml(JAXBContext jaxbContext, String xml, Map<String,Object> unmarshallerProperties); /** * As {@link #fromXml(JAXBContext, String)}, but downcast to a specific type. */ @Programmatic <T> T fromXml(Class<T> domainClass, String xml); /** * As {@link #fromXml(JAXBContext, String, Map)}, but downcast to a specific type. */ @Programmatic <T> T fromXml(Class<T> domainClass, String xml, Map<String,Object> unmarshallerProperties); @Programmatic String toXml(final Object domainObject); @Programmatic String toXml(final Object domainObject, Map<String,Object> marshallerProperties); /** * Controls whether, when generating {@link #toXsd(Object, IsisSchemas) XML schemas}, * any of the common Isis schemas (in the namespace <code>http://org.apache.isis.schema</code>) should be included * or just ignored (and therefore don't appear in the returned map). * * <p> * The practical benefit of this is that for many DTOs there will only be one other * schema, that of the DTO itself. The {@link Dto_downloadXsd} mixin uses this to return that single XSD, * rather than generating a ZIP of two schemas (the Isis schema and the one for the DTO), as it would otherwise; * far more convenient when debugging and so on. The Isis schemas can always be * <a href="http://isis.apache.org/schema">downloaded</a> from the Isis website. * </p> */ enum IsisSchemas { INCLUDE, IGNORE; /** * Implementation note: not using subclasses, otherwise the key in translations.po becomes more complex. */ public boolean shouldIgnore(final String namespaceUri) { if(this == INCLUDE) { return false; } else { return namespaceUri.matches(".*isis\\.apache\\.org.*"); } } } @Programmatic Map<String, String> toXsd(final Object domainObject, final IsisSchemas isisSchemas); public static class Simple implements JaxbService { @Override public Object fromXml(final JAXBContext jaxbContext, final String xml) { return fromXml(jaxbContext, xml, Maps.<String,Object>newHashMap()); } @Override public Object fromXml(final JAXBContext jaxbContext, final String xml, final Map<String, Object> unmarshallerProperties) { try { final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); for (Map.Entry<String, Object> entry : unmarshallerProperties.entrySet()) { unmarshaller.setProperty(entry.getKey(), entry.getValue()); } configure(unmarshaller); final Object unmarshal = unmarshaller.unmarshal(new StringReader(xml)); return unmarshal; } catch (final JAXBException ex) { throw new NonRecoverableException("Error unmarshalling XML", ex); } } @Override public <T> T fromXml(final Class<T> domainClass, final String xml) { return fromXml(domainClass, xml, Maps.<String,Object>newHashMap()); } @Override public <T> T fromXml(final Class<T> domainClass, final String xml, final Map<String, Object> unmarshallerProperties) { try { final JAXBContext context = JAXBContext.newInstance(domainClass); return (T) fromXml(context, xml, unmarshallerProperties); } catch (final JAXBException ex) { throw new NonRecoverableException("Error unmarshalling XML to class '" + domainClass.getName() + "'", ex); } } @Override public String toXml(final Object domainObject) { return toXml(domainObject, Maps.<String,Object>newHashMap()); } @Override public String toXml(final Object domainObject, final Map<String, Object> marshallerProperties) { final Class<?> domainClass = domainObject.getClass(); try { final JAXBContext context = JAXBContext.newInstance(domainClass); final Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); for (Map.Entry<String, Object> entry : marshallerProperties.entrySet()) { marshaller.setProperty(entry.getKey(), entry.getValue()); } configure(marshaller); final StringWriter sw = new StringWriter(); marshaller.marshal(domainObject, sw); final String xml = sw.toString(); return xml; } catch (final JAXBException ex) { final Class<? extends JAXBException> exClass = ex.getClass(); final String name = exClass.getName(); if(name.equals("com.sun.xml.bind.v2.runtime.IllegalAnnotationsException")) { // report a better error if possible // this is done reflectively so as to not have to bring in a new Maven dependency List<? extends Exception> errors = null; String annotationExceptionMessages = null; try { final Method getErrorsMethod = exClass.getMethod("getErrors"); errors = (List<? extends Exception>) getErrorsMethod.invoke(ex); annotationExceptionMessages = ": " + Joiner.on("; ").join( Iterables.transform(errors, new Function<Exception, String>() { @Override public String apply(final Exception e) { return e.getMessage(); } })); } catch (Exception e) { // fall through if we hit any snags, and instead throw the more generic error message. } if(errors != null) { throw new NonRecoverableException( "Error marshalling domain object to XML, due to illegal annotations on domain object class '" + domainClass.getName() + "'; " + errors.size() + " error" + (errors.size() == 1? "": "s") + " reported" + (!errors .isEmpty() ? annotationExceptionMessages : ""), ex); } } throw new NonRecoverableException("Error marshalling domain object to XML; domain object class is '" + domainClass.getName() + "'", ex); } } /** * Optional hook */ protected void configure(final Unmarshaller unmarshaller) { } /** * Optional hook */ protected void configure(final Marshaller marshaller) { } public Map<String,String> toXsd(final Object domainObject, final IsisSchemas isisSchemas) { try { final Class<?> domainClass = domainObject.getClass(); final JAXBContext context = JAXBContext.newInstance(domainClass); final CatalogingSchemaOutputResolver outputResolver = new CatalogingSchemaOutputResolver(isisSchemas); context.generateSchema(outputResolver); return outputResolver.asMap(); } catch (final JAXBException | IOException ex) { throw new ApplicationException(ex); } } @javax.inject.Inject DomainObjectContainer container; } }