/*
* Copyright 2015 herd contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.finra.herd.swaggergen;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlType;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
/**
* Generates example XML.
*/
public class ExampleXmlGenerator
{
// The log to use for logging purposes.
@SuppressWarnings("PMD.ProperLogger") // Logger is passed into this method from Mojo base class.
private Log log;
// An example date/time.
private static final GregorianCalendar EXAMPLE_GREGORIAN_CALENDAR = new GregorianCalendar(2015, 11, 25, 0, 0, 0);
// The call stack to determine if we're in an infinite loop or not.
private Stack<String> callStack = new Stack<>();
private String exampleXml;
/**
* Instantiates an example XML generator.
*
* @param log the log.
* @param clazz the class to generate the example XML on.
*
* @throws MojoExecutionException if any problems were encountered.
*/
public ExampleXmlGenerator(Log log, Class<?> clazz) throws MojoExecutionException
{
this.log = log;
generateExampleXml(clazz);
}
/**
* Gets an example XML body for the specified class.
*
* @param clazz the class.
*
* @throws MojoExecutionException if any problems were encountered.
*/
private void generateExampleXml(Class<?> clazz) throws MojoExecutionException
{
Object finalInstance = processClass(clazz);
try
{
if (finalInstance == null)
{
exampleXml = "";
log.info("Can't produce XML for a null element for class \"" + clazz + "\" so using the empty string.");
}
else
{
// Convert the instance to XML and remove the extra \r characters which causes problems when viewing in the Swagger UI
exampleXml = objectToXml(finalInstance).replaceAll("\r", "");
}
}
catch (JAXBException e)
{
throw new MojoExecutionException("Unable to serialize XML for class \"" + clazz.getName() + "\". Reason: " + e.getMessage(), e);
}
}
/**
* Processes a class by returning the "example" instance of the class.
*
* @param clazz the class.
*
* @return the instance of the class.
* @throws MojoExecutionException if any errors were encountered.
*/
private Object processClass(Class<?> clazz) throws MojoExecutionException
{
log.debug("Generating example XML for class \"" + clazz.getName() + "\".");
Object instance = null;
try
{
if (String.class.isAssignableFrom(clazz))
{
instance = "string";
}
else if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz))
{
instance = 0;
}
else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz))
{
instance = 0L;
}
else if (BigDecimal.class.isAssignableFrom(clazz))
{
instance = BigDecimal.ZERO;
}
else if (Double.class.isAssignableFrom(clazz))
{
instance = 0.0;
}
else if (XMLGregorianCalendar.class.isAssignableFrom(clazz))
{
DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
instance = datatypeFactory.newXMLGregorianCalendar(EXAMPLE_GREGORIAN_CALENDAR);
}
else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz))
{
instance = true;
}
else if (clazz.isEnum())
{
// If we have an enum, use the first value if at least one value is present.
Enum<?>[] enums = (Enum<?>[]) clazz.getEnumConstants();
if (enums != null && enums.length > 0)
{
instance = enums[0];
}
}
else if (clazz.getAnnotation(XmlType.class) != null)
{
// We can't instantiate interfaces.
if (!clazz.isInterface())
{
// Create a new instance of the class.
instance = clazz.newInstance();
// Process the fields of the class, but only if we haven't processed it before (via the call stack). This is to protect from an infinite
// loop.
if (!callStack.contains(clazz.getName()))
{
// Push this class on the stack to show we've processed this class.
callStack.push(clazz.getName());
// Process all the fields of the class.
for (Field field : clazz.getDeclaredFields())
{
field.setAccessible(true);
processField(instance, field);
}
// Remove this class from the stack since we've finished processing it.
callStack.pop();
}
}
}
else
{
// Default to string in all other cases.
instance = "string";
}
}
catch (IllegalAccessException | InstantiationException | DatatypeConfigurationException e)
{
throw new MojoExecutionException("Unable to create example XML for class \"" + clazz.getName() + "\". Reason: " + e.getMessage(), e);
}
return instance;
}
/**
* Processes a field of the example model.
*
* @param instance the instance to set.
* @param field the field to set the instance on.
*
* @throws MojoExecutionException if any problems were encountered.
*/
private void processField(Object instance, Field field) throws MojoExecutionException
{
try
{
log.debug("Processing field \"" + field.getName() + "\".");
if (!Modifier.isStatic(field.getModifiers()))
{
Class<?> fieldClass = field.getType();
if (Collection.class.isAssignableFrom(fieldClass))
{
Class<?> actualTypeClass = FieldUtils.getCollectionType(field);
if (List.class.isAssignableFrom(fieldClass))
{
List<Object> list = new ArrayList<>();
field.set(instance, list);
list.add(processClass(actualTypeClass));
}
else if (Set.class.isAssignableFrom(fieldClass))
{
Set<Object> set = new HashSet<>();
field.set(instance, set);
set.add(processClass(actualTypeClass));
}
}
else
{
field.set(instance, processClass(field.getType()));
}
}
}
catch (IllegalAccessException e)
{
throw new MojoExecutionException("Unable to process field \"" + field.getName() + "\". Reason: " + e.getMessage(), e);
}
}
/**
* Returns XML representation of the object.
*
* @param object the Java object to be serialized.
*
* @return the XML representation of this object
* @throws javax.xml.bind.JAXBException if a JAXB error occurred.
*/
private String objectToXml(Object object) throws JAXBException
{
JAXBContext requestContext = JAXBContext.newInstance(object.getClass());
Marshaller requestMarshaller = requestContext.createMarshaller();
requestMarshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
requestMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter sw = new StringWriter();
requestMarshaller.marshal(object, sw);
return sw.toString();
}
/**
* Gets the produces example XML.
*
* @return the example XML.
*/
public String getExampleXml()
{
return exampleXml;
}
}