001/**
002 * Copyright 2010-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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 * http://www.opensource.org/licenses/ecl2.php
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 */
016package org.kuali.common.util.xml.jaxb;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.io.UnsupportedEncodingException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import javax.xml.bind.JAXBContext;
032import javax.xml.bind.JAXBException;
033import javax.xml.bind.Marshaller;
034import javax.xml.bind.Unmarshaller;
035import javax.xml.bind.UnmarshallerHandler;
036import javax.xml.parsers.ParserConfigurationException;
037import javax.xml.parsers.SAXParser;
038import javax.xml.parsers.SAXParserFactory;
039
040import org.apache.commons.io.FileUtils;
041import org.apache.commons.io.IOUtils;
042import org.kuali.common.util.Assert;
043import org.kuali.common.util.CollectionUtils;
044import org.kuali.common.util.LocationUtils;
045import org.kuali.common.util.xml.service.XmlService;
046import org.xml.sax.InputSource;
047import org.xml.sax.SAXException;
048import org.xml.sax.XMLReader;
049
050public class JAXBXmlService implements XmlService {
051
052        private final boolean formatOutput;
053        private final boolean useNamespaceAwareParser;
054        private final Map<String, ?> properties;
055        private final boolean useEclipseLinkMoxyProvider;
056
057        @Override
058        public void write(File file, Object object) {
059                Assert.noNulls(file, object);
060                OutputStream out = null;
061                try {
062                        out = FileUtils.openOutputStream(file);
063                        write(out, object);
064                } catch (IOException e) {
065                        throw new IllegalStateException("Unexpected IO error", e);
066                } finally {
067                        IOUtils.closeQuietly(out);
068                }
069        }
070
071        @Override
072        public void write(OutputStream out, Object object) {
073                Assert.noNulls(out, object);
074                try {
075                        JAXBContext context = getJAXBContext(object.getClass());
076                        Marshaller marshaller = context.createMarshaller();
077                        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, formatOutput);
078                        marshaller.marshal(object, out);
079                } catch (JAXBException e) {
080                        throw new IllegalStateException("Unexpected JAXB error", e);
081                }
082        }
083
084        @Override
085        public <T> T getObjectFromXml(String xml, String encoding, Class<T> type) {
086                Assert.noBlanks(xml, encoding);
087                Assert.noNulls(type);
088                InputStream in = null;
089                try {
090                        in = new ByteArrayInputStream(xml.getBytes(encoding));
091                        return getObject(in, type);
092                } catch (IOException e) {
093                        throw new IllegalStateException("Unexpected IO error", e);
094                } finally {
095                        IOUtils.closeQuietly(in);
096                }
097        }
098
099        @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}