001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.http.codec.xml; 018 019import java.io.OutputStream; 020import java.nio.charset.StandardCharsets; 021import java.util.Map; 022import java.util.function.Function; 023 024import javax.xml.bind.JAXBException; 025import javax.xml.bind.MarshalException; 026import javax.xml.bind.Marshaller; 027import javax.xml.bind.annotation.XmlRootElement; 028import javax.xml.bind.annotation.XmlType; 029 030import reactor.core.publisher.Flux; 031import reactor.core.publisher.Mono; 032 033import org.springframework.core.ResolvableType; 034import org.springframework.core.codec.AbstractSingleValueEncoder; 035import org.springframework.core.codec.CodecException; 036import org.springframework.core.codec.EncodingException; 037import org.springframework.core.codec.Hints; 038import org.springframework.core.io.buffer.DataBuffer; 039import org.springframework.core.io.buffer.DataBufferFactory; 040import org.springframework.core.io.buffer.DataBufferUtils; 041import org.springframework.core.log.LogFormatUtils; 042import org.springframework.lang.Nullable; 043import org.springframework.util.ClassUtils; 044import org.springframework.util.MimeType; 045import org.springframework.util.MimeTypeUtils; 046 047/** 048 * Encode from single value to a byte stream containing XML elements. 049 * 050 * <p>{@link javax.xml.bind.annotation.XmlElements @XmlElements} and 051 * {@link javax.xml.bind.annotation.XmlElement @XmlElement} can be used 052 * to specify how collections should be marshalled. 053 * 054 * @author Sebastien Deleuze 055 * @author Arjen Poutsma 056 * @since 5.0 057 * @see Jaxb2XmlDecoder 058 */ 059public class Jaxb2XmlEncoder extends AbstractSingleValueEncoder<Object> { 060 061 private final JaxbContextContainer jaxbContexts = new JaxbContextContainer(); 062 063 private Function<Marshaller, Marshaller> marshallerProcessor = Function.identity(); 064 065 066 public Jaxb2XmlEncoder() { 067 super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); 068 } 069 070 071 /** 072 * Configure a processor function to customize Marshaller instances. 073 * @param processor the function to use 074 * @since 5.1.3 075 */ 076 public void setMarshallerProcessor(Function<Marshaller, Marshaller> processor) { 077 this.marshallerProcessor = this.marshallerProcessor.andThen(processor); 078 } 079 080 /** 081 * Return the configured processor for customizing Marshaller instances. 082 * @since 5.1.3 083 */ 084 public Function<Marshaller, Marshaller> getMarshallerProcessor() { 085 return this.marshallerProcessor; 086 } 087 088 089 @Override 090 public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { 091 if (super.canEncode(elementType, mimeType)) { 092 Class<?> outputClass = elementType.toClass(); 093 return (outputClass.isAnnotationPresent(XmlRootElement.class) || 094 outputClass.isAnnotationPresent(XmlType.class)); 095 } 096 else { 097 return false; 098 } 099 } 100 101 @Override 102 protected Flux<DataBuffer> encode(Object value, DataBufferFactory bufferFactory, 103 ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 104 105 // we're relying on doOnDiscard in base class 106 return Mono.fromCallable(() -> encodeValue(value, bufferFactory, valueType, mimeType, hints)).flux(); 107 } 108 109 @Override 110 public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, 111 ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 112 113 if (!Hints.isLoggingSuppressed(hints)) { 114 LogFormatUtils.traceDebug(logger, traceOn -> { 115 String formatted = LogFormatUtils.formatValue(value, !traceOn); 116 return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]"; 117 }); 118 } 119 120 boolean release = true; 121 DataBuffer buffer = bufferFactory.allocateBuffer(1024); 122 try { 123 OutputStream outputStream = buffer.asOutputStream(); 124 Class<?> clazz = ClassUtils.getUserClass(value); 125 Marshaller marshaller = initMarshaller(clazz); 126 marshaller.marshal(value, outputStream); 127 release = false; 128 return buffer; 129 } 130 catch (MarshalException ex) { 131 throw new EncodingException("Could not marshal " + value.getClass() + " to XML", ex); 132 } 133 catch (JAXBException ex) { 134 throw new CodecException("Invalid JAXB configuration", ex); 135 } 136 finally { 137 if (release) { 138 DataBufferUtils.release(buffer); 139 } 140 } 141 } 142 143 private Marshaller initMarshaller(Class<?> clazz) throws CodecException, JAXBException { 144 Marshaller marshaller = this.jaxbContexts.createMarshaller(clazz); 145 marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); 146 marshaller = this.marshallerProcessor.apply(marshaller); 147 return marshaller; 148 } 149 150}