001/**
002 * Copyright 2014 Tampere University of Technology, Pori Department
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 *   http://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 */
016package core.tut.pori.utils;
017
018import java.io.InputStream;
019import java.io.StringReader;
020import java.io.StringWriter;
021
022import javax.xml.bind.JAXBContext;
023import javax.xml.bind.JAXBException;
024import javax.xml.bind.Marshaller;
025import javax.xml.bind.Unmarshaller;
026import javax.xml.bind.ValidationEvent;
027import javax.xml.bind.ValidationEventHandler;
028import javax.xml.transform.OutputKeys;
029import javax.xml.transform.Transformer;
030import javax.xml.transform.TransformerException;
031import javax.xml.transform.TransformerFactory;
032import javax.xml.transform.TransformerFactoryConfigurationError;
033import javax.xml.transform.dom.DOMSource;
034import javax.xml.transform.stream.StreamResult;
035
036import org.apache.commons.lang3.ArrayUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.log4j.Logger;
039import org.w3c.dom.Document;
040import org.w3c.dom.Node;
041
042import core.tut.pori.http.Response;
043import core.tut.pori.http.ResponseData;
044
045
046/**
047 * XML formatter.
048 * 
049 * This class can be used to marshal objects to xml output, and unmarshal objects from xml input.
050 */
051public class XMLFormatter {
052  private static final Logger LOGGER = Logger.getLogger(XMLFormatter.class);
053  private boolean _omitXMLDeclaration = false;
054  private boolean _throwOnError = true;
055  
056  /**
057   * 
058   * @param string
059   * @param cls
060   * @return the object or null if bad/null/empty string
061   * @throws IllegalArgumentException on bad xml
062   */
063  @SuppressWarnings("unchecked")
064  public <T> T toObject(String string, Class<T> cls) throws IllegalArgumentException{
065    if(StringUtils.isBlank(string)){
066      return null;
067    }
068    T retval = null;
069    try (StringReader reader = new StringReader(string)) {
070      JAXBContext context = JAXBContext.newInstance(cls);
071      Unmarshaller um = createUnMarshaller(context);
072      Object o = um.unmarshal(reader);
073      if(o.getClass() != cls){
074        throw new IllegalArgumentException("Contents not of expected type.");
075      }else{
076        retval = (T) o;
077      }
078    } catch (JAXBException ex) {
079      LOGGER.error(ex, ex);
080      throw new IllegalArgumentException("Failed to parse xml.");
081    }
082    return retval;
083  }
084  
085  /**
086   * 
087   * @param in
088   * @param cls
089   * @return the object or null if bad/null in
090   * @throws IllegalArgumentException on bad xml
091   */
092  @SuppressWarnings("unchecked")
093  public <T> T toObject(InputStream in, Class<T> cls) throws IllegalArgumentException{
094    if(in == null){
095      return null;
096    }
097    T retval = null;
098    try{
099      JAXBContext context = JAXBContext.newInstance(cls);
100      Unmarshaller um = createUnMarshaller(context);
101      Object o = um.unmarshal(in);
102      if(o.getClass() != cls){
103        throw new IllegalArgumentException("Contents not of expected type.");
104      }else{
105        retval = (T) o;
106      }
107    } catch(JAXBException ex){
108      LOGGER.error(ex, ex);
109      throw new IllegalArgumentException("Failed to parse xml.");
110    }
111    return retval;
112  }
113  
114  /**
115   * 
116   * @param in
117   * @param objectClass class of the object to create
118   * @param requiredClasses additional classes required, note: objectClass is automatically added to requiredClasses
119   * @return the object or null if bad/null input
120   * @throws IllegalArgumentException on bad xml
121   */
122  @SuppressWarnings("unchecked")
123  public <T> T toObject(InputStream in, Class<T> objectClass, Class<?> ...requiredClasses) throws IllegalArgumentException{
124    if(in == null){
125      return null;
126    }
127    try{
128      JAXBContext context = JAXBContext.newInstance(ArrayUtils.add(requiredClasses, objectClass));
129      Unmarshaller um = createUnMarshaller(context);
130      Object o = um.unmarshal(in);
131      if(o.getClass() != objectClass){
132        throw new IllegalArgumentException("Contents not of expected type.");
133      }else{
134        return (T) o;
135      }
136    } catch(JAXBException ex){
137      LOGGER.error(ex, ex);
138      throw new IllegalArgumentException("Failed to parse xml.");
139    }
140  }
141  
142  /**
143   * 
144   * @param node
145   * @param cls
146   * @return the node as an object
147   * @throws IllegalArgumentException on bad xml
148   */
149  @SuppressWarnings("unchecked")
150  public <T> T toObject(Node node, Class<T> cls) throws IllegalArgumentException{
151    if(node == null){
152      return null;
153    }
154    T retval = null;
155    try{
156      JAXBContext context = JAXBContext.newInstance(cls);
157      Unmarshaller um = createUnMarshaller(context);
158      Object o = um.unmarshal(node);
159      if(o.getClass() != cls){
160        throw new IllegalArgumentException("Contents not of expected type.");
161      }else{
162        retval = (T) o;
163      }
164    } catch(JAXBException ex){
165      LOGGER.error(ex, ex);
166      throw new IllegalArgumentException("Failed to parse xml.");
167    }
168    return retval;
169  }
170  
171  /**
172   * 
173   * @param r annotated xml object
174   * @return the object as a xml string or null if bad object
175   */
176  public String toString(Response r){
177    String retval = null;
178    try {
179      JAXBContext context = null;
180      ResponseData t = r.getResponseData();
181      if(t == null){
182        context = JAXBContext.newInstance(Response.class);
183      }else{
184        context = JAXBContext.newInstance(ArrayUtils.add(t.getDataClasses(), Response.class));
185      } 
186      Marshaller marshaller = createMarshaller(context);
187      StringWriter w = new StringWriter();
188      marshaller.marshal(r, w);
189      retval = w.toString();
190    } catch (JAXBException ex) {
191      LOGGER.error(ex, ex);
192    }
193    
194    return retval;
195  }
196  
197  /**
198   * 
199   * @param o annotated xml object
200   * @return the object as a xml string or null if bad object
201   */
202  public <T> String toString(T o){
203    String retval = null;
204    try {
205      Marshaller marshaller = createMarshaller(JAXBContext.newInstance(o.getClass()));
206      StringWriter w = new StringWriter();
207      marshaller.marshal(o, w);
208      retval = w.toString();
209    } catch (JAXBException ex) {
210      LOGGER.error(ex, ex);
211    }
212    
213    return retval;
214  }
215  
216  /**
217   * 
218   * @param doc
219   * @return the document as string or null if not a valid document
220   */
221  public String toString(Document doc){
222    String result = null;
223    try {
224      Transformer tf = TransformerFactory.newInstance().newTransformer();
225      if(_omitXMLDeclaration){
226        tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
227      }else{
228        tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
229      }
230      StringWriter w = new StringWriter();
231      tf.transform(new DOMSource(doc), new StreamResult(w));
232      result = w.toString();
233    } catch (TransformerFactoryConfigurationError | TransformerException ex) {
234      LOGGER.error(ex, ex);
235    }
236    return result;
237  }
238  
239  /**
240   * create and return new marshaller, and set the default values
241   * @param context
242   * @return marshaller for the given context
243   * @throws JAXBException
244   * @throws IllegalArgumentException 
245   */
246  protected Marshaller createMarshaller(JAXBContext context) throws JAXBException, IllegalArgumentException{
247    Marshaller m = context.createMarshaller();
248    if(_throwOnError){
249      m.setEventHandler(new ValidationEventHandler() {
250        @Override
251        public boolean handleEvent(ValidationEvent event ) {
252          throw new IllegalArgumentException(event.getMessage(), event.getLinkedException());
253        }
254      });
255    }
256    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
257    m.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
258    if(_omitXMLDeclaration){
259      m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
260    }
261    return m;
262  }
263  
264  /**
265   * 
266   * @param context
267   * @return unmarshaller for the given context
268   * @throws JAXBException
269   * @throws IllegalArgumentException 
270   */
271  protected Unmarshaller createUnMarshaller(JAXBContext context) throws JAXBException, IllegalArgumentException{
272    Unmarshaller um = context.createUnmarshaller();
273    if(_throwOnError){
274      um.setEventHandler(new ValidationEventHandler() {
275        @Override
276        public boolean handleEvent(ValidationEvent event ) {
277          throw new IllegalArgumentException(event.getMessage(), event.getLinkedException());
278        }
279      });
280    }
281    return um;
282  }
283  
284  /**
285   * 
286   * @return true if xml declaration is omitted from the output
287   */
288  public boolean isOmitXMLDeclaration() {
289    return _omitXMLDeclaration;
290  }
291
292  /**
293   * 
294   * @param omitXML
295   */
296  public void setOmitXMLDeclaration(boolean omitXML) {
297    _omitXMLDeclaration = omitXML;
298  }
299
300  /**
301   * @return the throwOnError
302   */
303  public boolean isThrowOnError() {
304    return _throwOnError;
305  }
306
307  /**
308   * @param throwOnError the throwOnError to set
309   */
310  public void setThrowOnError(boolean throwOnError) {
311    _throwOnError = throwOnError;
312  }
313  
314  /**
315   * 
316   * @param in
317   * @param dataClass
318   * @return the object or null if bad/null input
319   * @throws IllegalArgumentException on bad xml
320   */
321  public Response toResponse(InputStream in, Class<?> dataClass) throws IllegalArgumentException{
322    if(in == null){
323      return null;
324    }
325    Response retval = toObject(in, Response.class, dataClass);
326    if(retval == null){
327      throw new IllegalArgumentException("Contents not of expected type.");
328    }
329    
330    ResponseData data = retval.getResponseData();
331    if(data == null){
332      LOGGER.warn("Response contains no data.");
333    }else if(!data.getClass().equals(dataClass)){
334      throw new IllegalArgumentException("Contents not of expected type: bad data.");
335    }
336    return retval;
337  }
338}