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}