/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.maven.packaging; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.sonatype.plexus.build.incremental.BuildContext; import static org.apache.camel.maven.packaging.PackageHelper.after; import static org.apache.camel.maven.packaging.PackageHelper.loadText; import static org.apache.camel.maven.packaging.PackageHelper.parseAsMap; /** * Analyses the Camel plugins in a project and generates extra descriptor information for easier auto-discovery in Camel. * * @goal generate-dataformats-list */ public class PackageDataFormatMojo extends AbstractMojo { /** * The maven project. * * @parameter property="project" * @required * @readonly */ protected MavenProject project; /** * The output directory for generated dataformats file * * @parameter default-value="${project.build.directory}/generated/camel/dataformats" */ protected File dataFormatOutDir; /** * The output directory for generated dataformats file * * @parameter default-value="${project.build.directory}/classes" */ protected File schemaOutDir; /** * Maven ProjectHelper. * * @component * @readonly */ private MavenProjectHelper projectHelper; /** * build context to check changed files and mark them for refresh (used for * m2e compatibility) * * @component * @readonly */ private BuildContext buildContext; /** * Execute goal. * * @throws org.apache.maven.plugin.MojoExecutionException execution of the main class or one of the * threads it generated failed. * @throws org.apache.maven.plugin.MojoFailureException something bad happened... */ public void execute() throws MojoExecutionException, MojoFailureException { prepareDataFormat(getLog(), project, projectHelper, dataFormatOutDir, schemaOutDir, buildContext); } public static void prepareDataFormat(Log log, MavenProject project, MavenProjectHelper projectHelper, File dataFormatOutDir, File schemaOutDir, BuildContext buildContext) throws MojoExecutionException { File camelMetaDir = new File(dataFormatOutDir, "META-INF/services/org/apache/camel/"); // first we need to setup the output directory because the next check // can stop the build before the end and eclipse always needs to know about that directory if (projectHelper != null) { projectHelper.addResource(project, dataFormatOutDir.getPath(), Collections.singletonList("**/dataformat.properties"), Collections.emptyList()); } if (!PackageHelper.haveResourcesChanged(log, project, buildContext, "META-INF/services/org/apache/camel/dataformat")) { return; } Map<String, String> javaTypes = new HashMap<String, String>(); StringBuilder buffer = new StringBuilder(); int count = 0; for (Resource r : project.getBuild().getResources()) { File f = new File(r.getDirectory()); if (!f.exists()) { f = new File(project.getBasedir(), r.getDirectory()); } f = new File(f, "META-INF/services/org/apache/camel/dataformat"); if (f.exists() && f.isDirectory()) { File[] files = f.listFiles(); if (files != null) { for (File file : files) { String javaType = readClassFromCamelResource(file, buffer, buildContext); if (!file.isDirectory() && file.getName().charAt(0) != '.') { count++; } if (javaType != null) { javaTypes.put(file.getName(), javaType); } } } } } // is this from Apache Camel then the data format is out of the box and we should enrich the json schema with more details boolean apacheCamel = "org.apache.camel".equals(project.getGroupId()); // find camel-core and grab the data format model from there, and enrich this model with information from this artifact // and create json schema model file for this data format try { if (apacheCamel && count > 0) { Artifact camelCore = findCamelCoreArtifact(project); if (camelCore != null) { File core = camelCore.getFile(); if (core != null) { URL url = new URL("file", null, core.getAbsolutePath()); URLClassLoader loader = new URLClassLoader(new URL[]{url}); for (Map.Entry<String, String> entry : javaTypes.entrySet()) { String name = entry.getKey(); String javaType = entry.getValue(); String modelName = asModelName(name); InputStream is = loader.getResourceAsStream("org/apache/camel/model/dataformat/" + modelName + ".json"); if (is == null) { // use file input stream if we build camel-core itself, and thus do not have a JAR which can be loaded by URLClassLoader is = new FileInputStream(new File(core, "org/apache/camel/model/dataformat/" + modelName + ".json")); } String json = loadText(is); DataFormatModel dataFormatModel = extractDataFormatModel(project, json, modelName, name, javaType); log.debug("Model " + dataFormatModel); // build json schema for the data format String properties = after(json, " \"properties\": {"); // special prepare for bindy/json properties properties = prepareBindyProperties(name, properties); properties = prepareJsonProperties(name, properties); String schema = createParameterJsonSchema(dataFormatModel, properties); log.debug("JSon schema\n" + schema); // write this to the directory File dir = new File(schemaOutDir, schemaSubDirectory(dataFormatModel.getJavaType())); dir.mkdirs(); File out = new File(dir, name + ".json"); OutputStream fos = buildContext.newFileOutputStream(out); fos.write(schema.getBytes()); fos.close(); log.debug("Generated " + out + " containing JSon schema for " + name + " data format"); } } } } } catch (Exception e) { throw new MojoExecutionException("Error loading dataformat model from camel-core. Reason: " + e, e); } if (count > 0) { Properties properties = new Properties(); String names = buffer.toString(); properties.put("dataFormats", names); properties.put("groupId", project.getGroupId()); properties.put("artifactId", project.getArtifactId()); properties.put("version", project.getVersion()); properties.put("projectName", project.getName()); if (project.getDescription() != null) { properties.put("projectDescription", project.getDescription()); } camelMetaDir.mkdirs(); File outFile = new File(camelMetaDir, "dataformat.properties"); // check if the existing file has the same content, and if so then leave it as is so we do not write any changes // which can cause a re-compile of all the source code if (outFile.exists()) { try { Properties existing = new Properties(); InputStream is = new FileInputStream(outFile); existing.load(is); is.close(); // are the content the same? if (existing.equals(properties)) { log.debug("No dataformat changes detected"); return; } } catch (IOException e) { // ignore } } try { OutputStream os = buildContext.newFileOutputStream(outFile); properties.store(os, "Generated by camel-package-maven-plugin"); os.close(); log.info("Generated " + outFile + " containing " + count + " Camel " + (count > 1 ? "dataformats: " : "dataformat: ") + names); } catch (IOException e) { throw new MojoExecutionException("Failed to write properties to " + outFile + ". Reason: " + e, e); } } else { log.debug("No META-INF/services/org/apache/camel/dataformat directory found. Are you sure you have created a Camel data format?"); } } private static DataFormatModel extractDataFormatModel(MavenProject project, String json, String modelName, String name, String javaType) throws Exception { DataFormatModel dataFormatModel = new DataFormatModel(); dataFormatModel.setName(name); dataFormatModel.setTitle(""); dataFormatModel.setModelName(modelName); dataFormatModel.setLabel(""); dataFormatModel.setDescription(project.getDescription()); dataFormatModel.setJavaType(javaType); dataFormatModel.setGroupId(project.getGroupId()); dataFormatModel.setArtifactId(project.getArtifactId()); dataFormatModel.setVersion(project.getVersion()); List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("model", json, false); for (Map<String, String> row : rows) { if (row.containsKey("title")) { String title = row.get("title"); dataFormatModel.setTitle(asModelTitle(name, title)); } if (row.containsKey("label")) { dataFormatModel.setLabel(row.get("label")); } if (row.containsKey("deprecated")) { dataFormatModel.setDeprecated(row.get("deprecated")); } if (row.containsKey("javaType")) { dataFormatModel.setModelJavaType(row.get("javaType")); } if (row.containsKey("firstVersion")) { dataFormatModel.setFirstVersion(row.get("firstVersion")); } // override description for camel-core, as otherwise its too generic if ("camel-core".equals(project.getArtifactId())) { if (row.containsKey("description")) { dataFormatModel.setDescription(row.get("description")); } } } // first version special for json String firstVersion = prepareJsonFirstVersion(name); if (firstVersion != null) { dataFormatModel.setFirstVersion(firstVersion); } return dataFormatModel; } private static String prepareBindyProperties(String name, String properties) { String bindy = "\"enum\": [ \"Csv\", \"Fixed\", \"KeyValue\" ], \"deprecated\": \"false\", \"secret\": \"false\""; String bindyCsv = "\"enum\": [ \"Csv\", \"Fixed\", \"KeyValue\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"Csv\""; String bindyFixed = "\"enum\": [ \"Csv\", \"Fixed\", \"KeyValue\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"Fixed\""; String bindyKvp = "\"enum\": [ \"Csv\", \"Fixed\", \"KeyValue\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"KeyValue\""; if ("bindy-csv".equals(name)) { properties = properties.replace(bindy, bindyCsv); } else if ("bindy-fixed".equals(name)) { properties = properties.replace(bindy, bindyFixed); } else if ("bindy-kvp".equals(name)) { properties = properties.replace(bindy, bindyKvp); } return properties; } private static String prepareJsonProperties(String name, String properties) { String json = "\"enum\": [ \"Gson\", \"Jackson\", \"Johnzon\", \"XStream\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"XStream\""; String jsonGson = "\"enum\": [ \"Gson\", \"Jackson\", \"Johnzon\", \"XStream\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"Gson\""; String jsonJackson = "\"enum\": [ \"Gson\", \"Jackson\", \"Johnzon\", \"XStream\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"Jackson\""; String jsonJohnzon = "\"enum\": [ \"Gson\", \"Jackson\", \"Johnzon\", \"XStream\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"Johnzon\""; String jsonXStream = "\"enum\": [ \"Gson\", \"Jackson\", \"Johnzon\", \"XStream\" ], \"deprecated\": \"false\", \"secret\": \"false\", \"defaultValue\": \"XStream\""; if ("json-gson".equals(name)) { properties = properties.replace(json, jsonGson); } else if ("json-jackson".equals(name)) { properties = properties.replace(json, jsonJackson); } else if ("json-johnzon".equals(name)) { properties = properties.replace(json, jsonJohnzon); } else if ("json-xstream".equals(name)) { properties = properties.replace(json, jsonXStream); } return properties; } private static String prepareJsonFirstVersion(String name) { if ("json-gson".equals(name)) { return "2.10.0"; } else if ("json-jackson".equals(name)) { return "2.0.0"; } else if ("json-johnzon".equals(name)) { return "2.18.0"; } else if ("json-xstream".equals(name)) { return "2.0.0"; } return null; } private static String readClassFromCamelResource(File file, StringBuilder buffer, BuildContext buildContext) throws MojoExecutionException { // skip directories as there may be a sub .resolver directory if (file.isDirectory()) { return null; } String name = file.getName(); if (name.charAt(0) != '.') { if (buffer.length() > 0) { buffer.append(" "); } buffer.append(name); } if (!buildContext.hasDelta(file)) { // if this file has not changed, // then no need to store the javatype // for the json file to be generated again // (but we do need the name above!) return null; } // find out the javaType for each data format try { String text = loadText(new FileInputStream(file)); Map<String, String> map = parseAsMap(text); return map.get("class"); } catch (IOException e) { throw new MojoExecutionException("Failed to read file " + file + ". Reason: " + e, e); } } private static String asModelName(String name) { // special for some data formats if ("json-gson".equals(name) || "json-jackson".equals(name) || "json-johnzon".equals(name) || "json-xstream".equals(name)) { return "json"; } else if ("bindy-csv".equals(name) || "bindy-fixed".equals(name) || "bindy-kvp".equals(name)) { return "bindy"; } else if ("zipfile".equals(name)) { // darn should have been lower case return "zipFile"; } else if ("yaml-snakeyaml".equals(name)) { return "yaml"; } return name; } private static String asModelTitle(String name, String title) { // special for some data formats if ("json-gson".equals(name)) { return "JSon GSon"; } else if ("json-jackson".equals(name)) { return "JSon Jackson"; } else if ("json-johnzon".equals(name)) { return "JSon Johnzon"; } else if ("json-xstream".equals(name)) { return "JSon XStream"; } else if ("bindy-csv".equals(name)) { return "Bindy CSV"; } else if ("bindy-fixed".equals(name)) { return "Bindy Fixed Length"; } else if ("bindy-kvp".equals(name)) { return "Bindy Key Value Pair"; } else if ("yaml-snakeyaml".equals(name)) { return "YAML SnakeYAML"; } return title; } private static Artifact findCamelCoreArtifact(MavenProject project) { // maybe this project is camel-core itself Artifact artifact = project.getArtifact(); if (artifact.getGroupId().equals("org.apache.camel") && artifact.getArtifactId().equals("camel-core")) { return artifact; } // or its a component which has a dependency to camel-core Iterator it = project.getDependencyArtifacts().iterator(); while (it.hasNext()) { artifact = (Artifact) it.next(); if (artifact.getGroupId().equals("org.apache.camel") && artifact.getArtifactId().equals("camel-core")) { return artifact; } } return null; } private static String schemaSubDirectory(String javaType) { int idx = javaType.lastIndexOf('.'); String pckName = javaType.substring(0, idx); return pckName.replace('.', '/'); } private static String createParameterJsonSchema(DataFormatModel dataFormatModel, String schema) { StringBuilder buffer = new StringBuilder("{"); // dataformat model buffer.append("\n \"dataformat\": {"); buffer.append("\n \"name\": \"").append(dataFormatModel.getName()).append("\","); buffer.append("\n \"kind\": \"").append("dataformat").append("\","); buffer.append("\n \"modelName\": \"").append(dataFormatModel.getModelName()).append("\","); if (dataFormatModel.getTitle() != null) { buffer.append("\n \"title\": \"").append(dataFormatModel.getTitle()).append("\","); } if (dataFormatModel.getDescription() != null) { buffer.append("\n \"description\": \"").append(dataFormatModel.getDescription()).append("\","); } boolean deprecated = "true".equals(dataFormatModel.getDeprecated()); buffer.append("\n \"deprecated\": ").append(deprecated).append(","); if (dataFormatModel.getFirstVersion() != null) { buffer.append("\n \"firstVersion\": \"").append(dataFormatModel.getFirstVersion()).append("\","); } buffer.append("\n \"label\": \"").append(dataFormatModel.getLabel()).append("\","); buffer.append("\n \"javaType\": \"").append(dataFormatModel.getJavaType()).append("\","); if (dataFormatModel.getModelJavaType() != null) { buffer.append("\n \"modelJavaType\": \"").append(dataFormatModel.getModelJavaType()).append("\","); } buffer.append("\n \"groupId\": \"").append(dataFormatModel.getGroupId()).append("\","); buffer.append("\n \"artifactId\": \"").append(dataFormatModel.getArtifactId()).append("\","); buffer.append("\n \"version\": \"").append(dataFormatModel.getVersion()).append("\""); buffer.append("\n },"); buffer.append("\n \"properties\": {"); buffer.append(schema); return buffer.toString(); } private static class DataFormatModel { private String name; private String title; private String modelName; private String description; private String firstVersion; private String label; private String deprecated; private String javaType; private String modelJavaType; private String groupId; private String artifactId; private String version; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getModelName() { return modelName; } public void setModelName(String modelName) { this.modelName = modelName; } public String getModelJavaType() { return modelJavaType; } public void setModelJavaType(String modelJavaType) { this.modelJavaType = modelJavaType; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getFirstVersion() { return firstVersion; } public void setFirstVersion(String firstVersion) { this.firstVersion = firstVersion; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public String getDeprecated() { return deprecated; } public void setDeprecated(String deprecated) { this.deprecated = deprecated; } public String getJavaType() { return javaType; } public void setJavaType(String javaType) { this.javaType = javaType; } public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public String getArtifactId() { return artifactId; } public void setArtifactId(String artifactId) { this.artifactId = artifactId; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } @Override public String toString() { return "DataFormatModel[" + "name='" + name + '\'' + ", title='" + title + '\'' + ", modelName='" + modelName + '\'' + ", description='" + description + '\'' + ", label='" + label + '\'' + ", deprecated='" + deprecated + '\'' + ", javaType='" + javaType + '\'' + ", modelJavaType='" + modelJavaType + '\'' + ", groupId='" + groupId + '\'' + ", artifactId='" + artifactId + '\'' + ", version='" + version + '\'' + ']'; } } }