View Javadoc
1   /**
2    * Copyright 2010-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.common.util.xml.jaxb;
17  
18  import java.io.ByteArrayInputStream;
19  import java.io.ByteArrayOutputStream;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  
31  import javax.xml.bind.JAXBContext;
32  import javax.xml.bind.JAXBException;
33  import javax.xml.bind.Marshaller;
34  import javax.xml.bind.Unmarshaller;
35  import javax.xml.bind.UnmarshallerHandler;
36  import javax.xml.parsers.ParserConfigurationException;
37  import javax.xml.parsers.SAXParser;
38  import javax.xml.parsers.SAXParserFactory;
39  
40  import org.apache.commons.io.FileUtils;
41  import org.apache.commons.io.IOUtils;
42  import org.kuali.common.util.Assert;
43  import org.kuali.common.util.CollectionUtils;
44  import org.kuali.common.util.LocationUtils;
45  import org.kuali.common.util.xml.service.XmlService;
46  import org.xml.sax.InputSource;
47  import org.xml.sax.SAXException;
48  import org.xml.sax.XMLReader;
49  
50  public class JAXBXmlService implements XmlService {
51  
52  	private final boolean formatOutput;
53  	private final boolean useNamespaceAwareParser;
54  	private final Map<String, ?> properties;
55  	private final boolean useEclipseLinkMoxyProvider;
56  
57  	@Override
58  	public void write(File file, Object object) {
59  		Assert.noNulls(file, object);
60  		OutputStream out = null;
61  		try {
62  			out = FileUtils.openOutputStream(file);
63  			write(out, object);
64  		} catch (IOException e) {
65  			throw new IllegalStateException("Unexpected IO error", e);
66  		} finally {
67  			IOUtils.closeQuietly(out);
68  		}
69  	}
70  
71  	@Override
72  	public void write(OutputStream out, Object object) {
73  		Assert.noNulls(out, object);
74  		try {
75  			JAXBContext context = getJAXBContext(object.getClass());
76  			Marshaller marshaller = context.createMarshaller();
77  			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, formatOutput);
78  			marshaller.marshal(object, out);
79  		} catch (JAXBException e) {
80  			throw new IllegalStateException("Unexpected JAXB error", e);
81  		}
82  	}
83  
84  	@Override
85  	public <T> T getObjectFromXml(String xml, String encoding, Class<T> type) {
86  		Assert.noBlanks(xml, encoding);
87  		Assert.noNulls(type);
88  		InputStream in = null;
89  		try {
90  			in = new ByteArrayInputStream(xml.getBytes(encoding));
91  			return getObject(in, type);
92  		} catch (IOException e) {
93  			throw new IllegalStateException("Unexpected IO error", e);
94  		} finally {
95  			IOUtils.closeQuietly(in);
96  		}
97  	}
98  
99  	@Override
100 	@SuppressWarnings("unchecked")
101 	public <T> T getObject(InputStream in, Class<T> type) {
102 		Assert.noNulls(in, type);
103 		try {
104 			Unmarshaller unmarshaller = getUnmarshaller(type);
105 			if (useNamespaceAwareParser) {
106 				return (T) unmarshaller.unmarshal(in);
107 			} else {
108 				UnmarshallerHandler unmarshallerHandler = unmarshaller.getUnmarshallerHandler();
109 				SAXParserFactory spf = SAXParserFactory.newInstance();
110 				SAXParser sp = spf.newSAXParser();
111 				XMLReader xr = sp.getXMLReader();
112 				xr.setContentHandler(unmarshallerHandler);
113 				InputSource xmlSource = new InputSource(in);
114 				xr.parse(xmlSource);
115 				return (T) unmarshallerHandler.getResult();
116 			}
117 		} catch (JAXBException e) {
118 			throw new IllegalStateException("Unexpected JAXB error", e);
119 		} catch (SAXException e) {
120 			throw new IllegalStateException("Unexpected SAX error", e);
121 		} catch (IOException e) {
122 			throw new IllegalStateException("Unexpected IO error", e);
123 		} catch (ParserConfigurationException e) {
124 			throw new IllegalStateException("Unexpected parser configuration error", e);
125 		}
126 	}
127 
128 	@Override
129 	public <T> T getObject(File file, Class<T> type) {
130 		Assert.exists(file);
131 		Assert.noNulls(type);
132 		return getObject(LocationUtils.getCanonicalPath(file), type);
133 	}
134 
135 	@Override
136 	public <T> T getObject(String location, Class<T> type) {
137 		Assert.noBlanks(location);
138 		Assert.noNulls(type);
139 		InputStream in = null;
140 		try {
141 			in = LocationUtils.getInputStream(location);
142 			return getObject(in, type);
143 		} catch (IOException e) {
144 			throw new IllegalStateException("Unexpected IO error", e);
145 		} finally {
146 			IOUtils.closeQuietly(in);
147 		}
148 	}
149 
150 	@Override
151 	public String toXml(Object object, String encoding) {
152 		Assert.noNulls(object);
153 		Assert.noBlanks(encoding);
154 		ByteArrayOutputStream out = new ByteArrayOutputStream();
155 		write(out, object);
156 		try {
157 			return out.toString(encoding);
158 		} catch (UnsupportedEncodingException e) {
159 			throw new IllegalArgumentException(e);
160 		}
161 	}
162 
163 	/**
164 	 * @deprecated Use toXml(object,encoding) instead
165 	 */
166 	@Override
167 	@Deprecated
168 	public String toString(Object object, String encoding) {
169 		return toXml(object, encoding);
170 	}
171 
172 	protected Unmarshaller getUnmarshaller(Class<?> clazz) throws JAXBException {
173 		Class<?>[] classes = getClassesToBeBound(clazz);
174 		JAXBContext jc = JAXBContext.newInstance(classes);
175 		return jc.createUnmarshaller();
176 	}
177 
178 	protected JAXBContext getJAXBContext(Class<?> clazz) throws JAXBException {
179 		Class<?>[] classes = getClassesToBeBound(clazz);
180 		if (properties.size() == 0) {
181 			return JAXBContext.newInstance(classes);
182 		} else {
183 			return JAXBContext.newInstance(classes, properties);
184 		}
185 	}
186 
187 	protected Class<?>[] getClassesToBeBound(Class<?> clazz) {
188 		List<Class<?>> classes = getClassesList(clazz);
189 		// Hack to get around https://java.net/jira/browse/JAXB-415
190 		if (useEclipseLinkMoxyProvider) {
191 			classes.add(0, UseEclipseLinkMoxyProvider.class);
192 		}
193 		return classes.toArray(new Class<?>[classes.size()]);
194 	}
195 
196 	protected List<Class<?>> getClassesList(Class<?> clazz) {
197 		XmlClassBindings bindings = clazz.getAnnotation(XmlClassBindings.class);
198 		List<Class<?>> classes = new ArrayList<Class<?>>(CollectionUtils.singletonList(clazz));
199 		if (bindings == null) {
200 			// base case
201 			return classes;
202 		} else {
203 			// recurse
204 			for (Class<?> binding : bindings.classes()) {
205 				classes.addAll(getClassesList(binding));
206 			}
207 			return classes;
208 		}
209 	}
210 
211 	public boolean isFormatOutput() {
212 		return formatOutput;
213 	}
214 
215 	public boolean isUseNamespaceAwareParser() {
216 		return useNamespaceAwareParser;
217 	}
218 
219 	public static Builder builder() {
220 		return new Builder();
221 	}
222 
223 	public static class Builder {
224 
225 		private static final Map<String, ?> EMPTY_MAP = Collections.unmodifiableMap(new HashMap<String, Object>());
226 
227 		public static final boolean FORMAT_OUTPUT = true;
228 		public static final boolean USE_NAMESPACE_AWARE_PARSER = true;
229 		public static final boolean USE_ECLIPSE_LINK_MOXY_PROVIDER = true;
230 
231 		// Optional fields with default values
232 		private boolean formatOutput = FORMAT_OUTPUT;
233 		private Map<String, ?> properties = EMPTY_MAP;
234 
235 		// Set this to false if you need to parse a log4j.xml file
236 		// log4j.xml has an attribute containing the colon character in the name of the attribute itself
237 		private boolean useNamespaceAwareParser = USE_NAMESPACE_AWARE_PARSER;
238 
239 		// This flag switches the service to use EclipseLink MOXy instead of the JAXB reference implementation that ships with the JDK
240 		// The *reason* for defaulting the service to MOXy is there is a pretty serious bug in the RI.
241 		// The RI throws an NPE if an adapter adapts a non-null bound value to null.
242 		// The jira for this issue is -> https://java.net/jira/browse/JAXB-415
243 		// This issue still occurs on the latest version of JDK7 as of August 24, 2013 (version 1.7.0_25-b15)
244 		// There are two detailed discussions of this issue and how to get around it using MOXy on stackoverflow
245 		// http://stackoverflow.com/questions/11894193/jaxb-marshal-empty-string-to-null-globally
246 		// http://stackoverflow.com/questions/6110612/how-to-prevent-marshalling-empty-tags-in-jaxb-when-string-is-empty-but-not-null
247 		// If the unit test related to this in kuali-util (JAXBIssue415Test.java) starts passing, we could switch back to the RI
248 		private boolean useEclipseLinkMoxyProvider = USE_ECLIPSE_LINK_MOXY_PROVIDER;
249 
250 		public Builder useEclipseLinkMoxyProvider(boolean useEclipseLinkMoxyProvider) {
251 			this.useEclipseLinkMoxyProvider = useEclipseLinkMoxyProvider;
252 			return this;
253 		}
254 
255 		public Builder formatOutput(boolean formatOutput) {
256 			this.formatOutput = formatOutput;
257 			return this;
258 		}
259 
260 		public Builder useNamespaceAwareParser(boolean useNamespaceAwareParser) {
261 			this.useNamespaceAwareParser = useNamespaceAwareParser;
262 			return this;
263 		}
264 
265 		public Builder properties(Map<String, ?> properties) {
266 			this.properties = properties;
267 			return this;
268 		}
269 
270 		public JAXBXmlService build() {
271 			Assert.noNulls(properties);
272 			this.properties = Collections.unmodifiableMap(new HashMap<String, Object>(properties));
273 			return new JAXBXmlService(this);
274 		}
275 
276 	}
277 
278 	private JAXBXmlService(Builder builder) {
279 		this.formatOutput = builder.formatOutput;
280 		this.useNamespaceAwareParser = builder.useNamespaceAwareParser;
281 		this.useEclipseLinkMoxyProvider = builder.useEclipseLinkMoxyProvider;
282 		this.properties = builder.properties;
283 	}
284 
285 	public Map<String, ?> getProperties() {
286 		return properties;
287 	}
288 
289 	public boolean isUseEclipseLinkMoxyProvider() {
290 		return useEclipseLinkMoxyProvider;
291 	}
292 
293 	private static class UseEclipseLinkMoxyProvider {
294 
295 		/**
296 		 * <p>
297 		 * The only purpose for this class is to make JAXB bootstrap itself with the jaxb.properties file in this directory and thus switch from the reference implementation to the
298 		 * EclipseLink MOXy implementation.
299 		 * </p>
300 		 * 
301 		 * <p>
302 		 * If issue https://java.net/jira/browse/JAXB-415 gets resolved, this class and the corresponding jaxb.properties file can be removed, and the useEclipseLinkMoxyProvider
303 		 * flag can be defaulted to false.
304 		 * </p>
305 		 */
306 	}
307 
308 }