| /* |
| * 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.polygene.serialization.javaxxml; |
| |
| import java.io.IOException; |
| import java.io.UncheckedIOException; |
| import java.io.Writer; |
| import java.util.Base64; |
| import java.util.Map; |
| import java.util.function.Function; |
| import java.util.stream.Stream; |
| import java.util.stream.StreamSupport; |
| import javax.xml.transform.TransformerException; |
| import javax.xml.transform.dom.DOMSource; |
| import javax.xml.transform.stream.StreamResult; |
| import org.apache.polygene.api.PolygeneAPI; |
| import org.apache.polygene.api.association.AssociationStateHolder; |
| import org.apache.polygene.api.common.Optional; |
| import org.apache.polygene.api.composite.Composite; |
| import org.apache.polygene.api.composite.CompositeInstance; |
| import org.apache.polygene.api.composite.StatefulAssociationCompositeDescriptor; |
| import org.apache.polygene.api.entity.EntityReference; |
| import org.apache.polygene.api.injection.scope.This; |
| import org.apache.polygene.api.injection.scope.Uses; |
| import org.apache.polygene.api.mixin.Initializable; |
| import org.apache.polygene.api.serialization.Converter; |
| import org.apache.polygene.api.serialization.Converters; |
| import org.apache.polygene.api.serialization.SerializationException; |
| import org.apache.polygene.api.service.ServiceDescriptor; |
| import org.apache.polygene.api.type.ArrayType; |
| import org.apache.polygene.api.type.EnumType; |
| import org.apache.polygene.api.type.MapType; |
| import org.apache.polygene.api.type.StatefulAssociationValueType; |
| import org.apache.polygene.spi.serialization.AbstractTextSerializer; |
| import org.apache.polygene.spi.serialization.XmlSerializer; |
| import org.apache.polygene.spi.util.ArrayIterable; |
| import org.w3c.dom.DOMException; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.apache.polygene.api.util.Collectors.toMap; |
| |
| /** |
| * XML Serializer. |
| */ |
| public class JavaxXmlSerializer extends AbstractTextSerializer |
| implements XmlSerializer, Initializable |
| { |
| private static final String NULL_ELEMENT_NAME = "null"; |
| |
| @This |
| private JavaxXmlFactories xmlFactories; |
| |
| @This |
| private Converters converters; |
| |
| @This |
| private JavaxXmlAdapters adapters; |
| |
| @Uses |
| private ServiceDescriptor descriptor; |
| |
| private JavaxXmlSettings settings; |
| |
| @Override |
| public void initialize() throws Exception |
| { |
| settings = JavaxXmlSettings.orDefault( descriptor.metaInfo( JavaxXmlSettings.class ) ); |
| } |
| |
| @Override |
| public void serialize( Options options, Writer writer, @Optional Object object ) |
| { |
| Document xmlDocument = toXml( options, object ); |
| if( xmlDocument == null ) |
| { |
| return; |
| } |
| try |
| { |
| // We want plain text nodes to be serialized without surrounding elements |
| if( xmlDocument.getNodeType() == Node.TEXT_NODE ) |
| { |
| writer.write( xmlDocument.getNodeValue() ); |
| } |
| else |
| { |
| xmlFactories.serializationTransformer().transform( new DOMSource( xmlDocument ), |
| new StreamResult( writer ) ); |
| } |
| } |
| catch( IOException ex ) |
| { |
| throw new UncheckedIOException( ex ); |
| } |
| catch( TransformerException ex ) |
| { |
| throw new SerializationException( "Unable to transform XML Document to String", ex ); |
| } |
| } |
| |
| @Override |
| public <T> Function<T, Document> toXmlFunction( Options options ) |
| { |
| return object -> doSerializeRoot( options, object ); |
| } |
| |
| private <T> Document doSerializeRoot( Options options, T object ) |
| { |
| Document doc = xmlFactories.newDocumentForSerialization(); |
| Element stateElement = doc.createElement( settings.getRootTagName() ); |
| Node node = doSerialize( doc, options, object, true ); |
| stateElement.appendChild( node ); |
| doc.appendChild( stateElement ); |
| return doc; |
| } |
| |
| private <T> Node doSerialize( Document document, Options options, T object, boolean root ) |
| { |
| if( object == null ) |
| { |
| return document.createElement( NULL_ELEMENT_NAME ); |
| } |
| Class<?> objectClass = object.getClass(); |
| Converter<Object> converter = converters.converterFor( objectClass ); |
| if( converter != null ) |
| { |
| return doSerialize( document, options, converter.toString( object ), false ); |
| } |
| JavaxXmlAdapter<?> adapter = adapters.adapterFor( objectClass ); |
| if( adapter != null ) |
| { |
| return adapter.serialize( document, object, value -> doSerialize( document, options, value, false ) ); |
| } |
| if( EnumType.isEnum( objectClass ) ) |
| { |
| return document.createTextNode( object.toString() ); |
| } |
| if( StatefulAssociationValueType.isStatefulAssociationValue( objectClass ) ) |
| { |
| return serializeStatefulAssociationValue( document, options, object, root ); |
| } |
| if( MapType.isMap( objectClass ) ) |
| { |
| return serializeMap( document, options, (Map<?, ?>) object ); |
| } |
| if( ArrayType.isArray( objectClass ) ) |
| { |
| return serializeArray( document, options, object ); |
| } |
| if( Iterable.class.isAssignableFrom( objectClass ) ) |
| { |
| return serializeIterable( document, options, (Iterable<?>) object ); |
| } |
| if( Stream.class.isAssignableFrom( objectClass ) ) |
| { |
| return serializeStream( document, options, (Stream<?>) object ); |
| } |
| throw new SerializationException( "Don't know how to serialize " + object ); |
| } |
| |
| private <T> Node serializeStatefulAssociationValue( Document document, Options options, T composite, boolean root ) |
| { |
| CompositeInstance instance = PolygeneAPI.FUNCTION_COMPOSITE_INSTANCE_OF.apply( (Composite) composite ); |
| StatefulAssociationCompositeDescriptor descriptor = |
| (StatefulAssociationCompositeDescriptor) instance.descriptor(); |
| AssociationStateHolder state = (AssociationStateHolder) instance.state(); |
| StatefulAssociationValueType<?> valueType = descriptor.valueType(); |
| |
| Element valueElement = document.createElement( settings.getValueTagName() ); |
| valueType.properties().forEach( |
| property -> |
| { |
| Object value = state.propertyFor( property.accessor() ).get(); |
| Converter<Object> converter = converters.converterFor( property ); |
| if( converter != null ) |
| { |
| value = converter.toString( value ); |
| } |
| Element element = document.createElement( property.qualifiedName().name() ); |
| element.appendChild( doSerialize( document, options, value, false ) ); |
| valueElement.appendChild( element ); |
| } ); |
| valueType.associations().forEach( |
| association -> |
| { |
| EntityReference value = state.associationFor( association.accessor() ).reference(); |
| Element element = document.createElement( association.qualifiedName().name() ); |
| element.appendChild( doSerialize( document, options, value, false ) ); |
| valueElement.appendChild( element ); |
| } |
| ); |
| valueType.manyAssociations().forEach( |
| association -> |
| { |
| Stream<EntityReference> value = state.manyAssociationFor( association.accessor() ).references(); |
| Element element = document.createElement( association.qualifiedName().name() ); |
| element.appendChild( doSerialize( document, options, value, false ) ); |
| valueElement.appendChild( element ); |
| } |
| ); |
| valueType.namedAssociations().forEach( |
| association -> |
| { |
| Map<String, EntityReference> value = state.namedAssociationFor( association.accessor() ).references() |
| .collect( toMap() ); |
| Element element = document.createElement( association.qualifiedName().name() ); |
| element.appendChild( doSerialize( document, options, value, false ) ); |
| valueElement.appendChild( element ); |
| } |
| ); |
| if( ( root && options.rootTypeInfo() ) || ( !root && options.nestedTypeInfo() ) ) |
| { |
| valueElement.setAttribute( settings.getTypeInfoTagName(), valueType.primaryType().getName() ); |
| } |
| return valueElement; |
| } |
| |
| private Node serializeMap( Document document, Options options, Map<?, ?> map ) |
| { |
| Element mapElement = document.createElement( settings.getMapTagName() ); |
| if( map.isEmpty() ) |
| { |
| return mapElement; |
| } |
| Function<Map.Entry, Node> complexMapping = entry -> |
| { |
| Element entryElement = document.createElement( settings.getMapEntryTagName() ); |
| |
| Element keyElement = document.createElement( "key" ); |
| keyElement.appendChild( doSerialize( document, options, entry.getKey(), false ) ); |
| entryElement.appendChild( keyElement ); |
| |
| Element valueElement = document.createElement( "value" ); |
| valueElement.appendChild( doSerialize( document, options, entry.getValue(), false ) ); |
| entryElement.appendChild( valueElement ); |
| |
| return entryElement; |
| }; |
| |
| if( map.keySet().iterator().next() instanceof CharSequence ) |
| { |
| map.entrySet().stream() |
| .map( entry -> |
| { |
| try |
| { |
| Element element = document.createElement( entry.getKey().toString() ); |
| element.appendChild( doSerialize( document, options, entry.getValue(), false ) ); |
| return element; |
| } |
| catch( DOMException ex ) |
| { |
| // The key name cannot be encoded as a tag name, fallback to complex mapping |
| // Tag names cannot start with a digit, some characters cannot be escaped etc... |
| return complexMapping.apply( entry ); |
| } |
| } ) |
| .forEach( mapElement::appendChild ); |
| } |
| else |
| { |
| map.entrySet().stream() |
| .map( complexMapping ) |
| .forEach( mapElement::appendChild ); |
| } |
| return mapElement; |
| } |
| |
| private <T> Node serializeArray( Document document, Options options, T object ) |
| { |
| ArrayType valueType = ArrayType.of( object.getClass() ); |
| if( valueType.isArrayOfPrimitiveBytes() ) |
| { |
| byte[] base64 = Base64.getEncoder().encode( (byte[]) object ); |
| return document.createCDATASection( new String( base64, UTF_8 ) ); |
| } |
| if( valueType.isArrayOfPrimitives() ) |
| { |
| return serializeIterable( document, options, new ArrayIterable( object ) ); |
| } |
| return serializeStream( document, options, Stream.of( (Object[]) object ) ); |
| } |
| |
| private Node serializeIterable( Document document, Options options, Iterable<?> object ) |
| { |
| return serializeStream( document, options, StreamSupport.stream( object.spliterator(), false ) ); |
| } |
| |
| private Node serializeStream( Document document, Options options, Stream<?> object ) |
| { |
| Element collectionElement = document.createElement( settings.getCollectionTagName() ); |
| object.map( each -> doSerialize( document, options, each, false ) ) |
| .forEach( itemValueNode -> |
| { |
| Element itemElement = document.createElement( settings.getCollectionElementTagName() ); |
| itemElement.appendChild( itemValueNode ); |
| collectionElement.appendChild( itemElement ); |
| } ); |
| return collectionElement; |
| } |
| } |