package japicmp.output.xml; import com.google.common.base.Joiner; import com.google.common.base.Optional; import japicmp.config.Options; import japicmp.exception.JApiCmpException; import japicmp.exception.JApiCmpException.Reason; import japicmp.filter.Filter; import japicmp.model.JApiClass; import japicmp.output.OutputFilter; import japicmp.output.OutputGenerator; import japicmp.output.extapi.jpa.JpaAnalyzer; import japicmp.output.extapi.jpa.model.JpaTable; import japicmp.output.xml.model.JApiCmpXmlRoot; import japicmp.util.Streams; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.SchemaOutputResolver; import javax.xml.transform.*; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.*; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; public class XmlOutputGenerator extends OutputGenerator<XmlOutput> { private static final String XSD_FILENAME = "japicmp.xsd"; private static final String XML_SCHEMA = XSD_FILENAME; private static final Logger LOGGER = Logger.getLogger(XmlOutputGenerator.class.getName()); private final XmlOutputGeneratorOptions xmlOutputGeneratorOptions; @Deprecated public XmlOutputGenerator(List<JApiClass> jApiClasses, Options options, boolean createSchemaFile) { super(options, jApiClasses); this.xmlOutputGeneratorOptions = new XmlOutputGeneratorOptions(); this.xmlOutputGeneratorOptions.setCreateSchemaFile(createSchemaFile); } public XmlOutputGenerator(List<JApiClass> jApiClasses, Options options, XmlOutputGeneratorOptions xmlOutputGeneratorOptions) { super(options, jApiClasses); this.xmlOutputGeneratorOptions = xmlOutputGeneratorOptions; } @Override public XmlOutput generate() { JApiCmpXmlRoot jApiCmpXmlRoot = createRootElement(jApiClasses, options); //analyzeJpaAnnotations(jApiCmpXmlRoot, jApiClasses); filterClasses(jApiClasses, options); return createXmlDocumentAndSchema(options, jApiCmpXmlRoot); } public static List<File> writeToFiles(Options options, XmlOutput xmlOutput) { List<File> filesWritten = new ArrayList<>(); try { if (xmlOutput.getXmlOutputStream().isPresent() && options.getXmlOutputFile().isPresent()) { File xmlFile = new File(options.getXmlOutputFile().get()); try (FileOutputStream fos = new FileOutputStream(xmlFile)) { ByteArrayOutputStream outputStream = xmlOutput.getXmlOutputStream().get(); outputStream.writeTo(fos); filesWritten.add(xmlFile); } catch (IOException e) { throw new JApiCmpException(JApiCmpException.Reason.IoException, "Failed to write XML file '" + xmlFile.getAbsolutePath() + "': " + e.getMessage(), e); } } if (xmlOutput.getHtmlOutputStream().isPresent() && options.getHtmlOutputFile().isPresent()) { File htmlFile = new File(options.getHtmlOutputFile().get()); try (FileOutputStream fos = new FileOutputStream(htmlFile)) { ByteArrayOutputStream outputStream = xmlOutput.getHtmlOutputStream().get(); outputStream.writeTo(fos); filesWritten.add(htmlFile); } catch (IOException e) { throw new JApiCmpException(JApiCmpException.Reason.IoException, "Failed to write HTML file '" + htmlFile.getAbsolutePath() + "': " + e.getMessage(), e); } } } finally { try { xmlOutput.close(); } catch (Exception e) { LOGGER.log(Level.FINE, "Failed to close XML file: " + e.getLocalizedMessage(), e); } } return filesWritten; } private void analyzeJpaAnnotations(JApiCmpXmlRoot jApiCmpXmlRoot, List<JApiClass> jApiClasses) { JpaAnalyzer jpaAnalyzer = new JpaAnalyzer(); List<JpaTable> jpaEntities = jpaAnalyzer.analyze(jApiClasses); //jApiCmpXmlRoot.setJpaTables(jpaEntities); } private XmlOutput createXmlDocumentAndSchema(Options options, JApiCmpXmlRoot jApiCmpXmlRoot) { XmlOutput xmlOutput = new XmlOutput(); xmlOutput.setJApiCmpXmlRoot(jApiCmpXmlRoot); ByteArrayOutputStream xmlBaos = null; InputStream styleSheetAsInputStream = null; InputStream xsltAsInputStream = null; try { JAXBContext jaxbContext = JAXBContext.newInstance(JApiCmpXmlRoot.class); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, XML_SCHEMA); xmlBaos = new ByteArrayOutputStream(); marshaller.marshal(jApiCmpXmlRoot, xmlBaos); if (options.getXmlOutputFile().isPresent()) { xmlOutput.setXmlOutputStream(Optional.of(xmlBaos)); if (xmlOutputGeneratorOptions.isCreateSchemaFile()) { final File xmlFile = new File(options.getXmlOutputFile().get()); SchemaOutputResolver outputResolver = new SchemaOutputResolver() { @Override public Result createOutput(String namespaceUri, String suggestedFileName) throws IOException { File schemaFile = xmlFile.getParentFile(); if (schemaFile == null) { LOGGER.warning(String.format("File '%s' has no parent file. Using instead: '%s'.", xmlFile.getAbsolutePath(), XSD_FILENAME)); schemaFile = new File(XSD_FILENAME); } else { schemaFile = new File(schemaFile + File.separator + XSD_FILENAME); } StreamResult result = new StreamResult(schemaFile); result.setSystemId(schemaFile.getAbsolutePath()); return result; } }; jaxbContext.generateSchema(outputResolver); } } if (options.getHtmlOutputFile().isPresent()) { TransformerFactory transformerFactory = TransformerFactory.newInstance(); xsltAsInputStream = XmlOutputGenerator.class.getResourceAsStream("/html.xslt"); if (xsltAsInputStream == null) { throw new JApiCmpException(Reason.XsltError, "Failed to load XSLT."); } if (options.getHtmlStylesheet().isPresent()) { styleSheetAsInputStream = new FileInputStream(options.getHtmlStylesheet().get()); } else { styleSheetAsInputStream = XmlOutputGenerator.class.getResourceAsStream("/style.css"); if (styleSheetAsInputStream == null) { throw new JApiCmpException(Reason.XsltError, "Failed to load stylesheet."); } } String xsltAsString = integrateStylesheetIntoXslt(xsltAsInputStream, styleSheetAsInputStream); Transformer transformer = transformerFactory.newTransformer(new StreamSource(new StringReader(xsltAsString))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xmlBaos.toByteArray()); ByteArrayOutputStream htmlOutputStream = new ByteArrayOutputStream(); transformer.transform(new StreamSource(byteArrayInputStream), new StreamResult(htmlOutputStream)); xmlOutput.setHtmlOutputStream(Optional.of(htmlOutputStream)); } } catch (JAXBException e) { throw new JApiCmpException(Reason.JaxbException, String.format("Marshalling of XML document failed: %s", e.getMessage()), e); } catch (IOException e) { throw new JApiCmpException(Reason.IoException, String.format("Marshalling of XML document failed: %s", e.getMessage()), e); } catch (TransformerConfigurationException e) { throw new JApiCmpException(Reason.XsltError, String.format("Configuration of XSLT transformer failed: %s", e.getMessage()), e); } catch (TransformerException e) { throw new JApiCmpException(Reason.XsltError, String.format("XSLT transformation failed: %s", e.getMessage()), e); } finally { try { if (styleSheetAsInputStream != null) { styleSheetAsInputStream.close(); } if (xsltAsInputStream != null) { xsltAsInputStream.close(); } } catch (IOException e) { LOGGER.log(Level.FINE, "Failed to close CSS and/or XSLT file: " + e.getLocalizedMessage(), e); } } return xmlOutput; } private String integrateStylesheetIntoXslt(InputStream xsltAsInputStream, InputStream styleSheetAsInputStream) { String xsltAsString = Streams.asString(xsltAsInputStream); String styleSheetAsString = Streams.asString(styleSheetAsInputStream); xsltAsString = xsltAsString.replace("<style type=\"text/css\"></style>", "<style type=\"text/css\">\n" + styleSheetAsString + "\n</style>"); if (System.getProperty("japicmp.dump.xslt") != null) { try { Files.write(Paths.get(System.getProperty("japicmp.dump.xslt")), Arrays.asList(xsltAsString), Charset.forName("UTF-8"), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException e) { LOGGER.log(Level.WARNING, "Could not dump XSLT file: " + e.getMessage(), e); } } return xsltAsString; } private void filterClasses(List<JApiClass> jApiClasses, Options options) { OutputFilter outputFilter = new OutputFilter(options); outputFilter.filter(jApiClasses); } private JApiCmpXmlRoot createRootElement(List<JApiClass> jApiClasses, Options options) { JApiCmpXmlRoot jApiCmpXmlRoot = new JApiCmpXmlRoot(); jApiCmpXmlRoot.setOldJar(options.joinOldArchives()); jApiCmpXmlRoot.setNewJar(options.joinNewArchives()); jApiCmpXmlRoot.setOldVersion(options.joinOldVersions()); jApiCmpXmlRoot.setNewVersion(options.joinNewVersions()); jApiCmpXmlRoot.setClasses(jApiClasses); jApiCmpXmlRoot.setAccessModifier(options.getAccessModifier().name()); jApiCmpXmlRoot.setOnlyModifications(options.isOutputOnlyModifications()); jApiCmpXmlRoot.setOnlyBinaryIncompatibleModifications(options.isOutputOnlyBinaryIncompatibleModifications()); jApiCmpXmlRoot.setPackagesInclude(filtersAsString(options.getIncludes(), true)); jApiCmpXmlRoot.setPackagesExclude(filtersAsString(options.getExcludes(), false)); jApiCmpXmlRoot.setIgnoreMissingClasses(options.getIgnoreMissingClasses().isIgnoreAllMissingClasses()); jApiCmpXmlRoot.setIgnoreMissingClassesByRegularExpressions(regExAsString(options.getIgnoreMissingClasses().getIgnoreMissingClassRegularExpression())); if (xmlOutputGeneratorOptions.getTitle().isPresent()) { jApiCmpXmlRoot.setTitle(xmlOutputGeneratorOptions.getTitle().get()); } jApiCmpXmlRoot.setSemanticVersioning(xmlOutputGeneratorOptions.getSemanticVersioningInformation()); return jApiCmpXmlRoot; } private String regExAsString(List<Pattern> ignoreMissingClassRegularExpression) { StringBuilder sb = new StringBuilder(); for (Pattern pattern : ignoreMissingClassRegularExpression) { if (sb.length() > 0) { sb.append(";"); } sb.append(pattern.toString()); } return sb.toString(); } private String filtersAsString(List<Filter> filters, boolean include) { String join; if (filters.size() == 0) { if (include) { join = "all"; } else { join = "n.a."; } } else { join = Joiner.on(";").skipNulls().join(filters); } return join; } }