org.apache.camel.maven.packaging.PackageDataFormatMojo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of camel-package-maven-plugin Show documentation
Show all versions of camel-package-maven-plugin Show documentation
Maven plugin to help package Camel components and plugins
The newest version!
/*
* 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.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.apache.camel.maven.packaging.generics.ClassUtil;
import org.apache.camel.spi.Metadata;
import org.apache.camel.tooling.model.DataFormatModel;
import org.apache.camel.tooling.model.DataFormatModel.DataFormatOptionModel;
import org.apache.camel.tooling.model.EipModel;
import org.apache.camel.tooling.model.EipModel.EipOptionModel;
import org.apache.camel.tooling.model.JsonMapper;
import org.apache.camel.tooling.model.SupportLevel;
import org.apache.camel.tooling.util.JavadocHelper;
import org.apache.camel.tooling.util.PackageHelper;
import org.apache.camel.tooling.util.Strings;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.build.BuildContext;
import org.jboss.forge.roaster.Roaster;
import org.jboss.forge.roaster.model.source.FieldSource;
import org.jboss.forge.roaster.model.source.JavaClassSource;
import org.jboss.forge.roaster.model.source.MethodSource;
/**
* Analyses the Camel plugins in a project and generates extra descriptor information for easier auto-discovery in
* Camel.
*/
@Mojo(name = "generate-dataformats-list", threadSafe = true,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class PackageDataFormatMojo extends AbstractGeneratorMojo {
/**
* The output directory for the generated data format resources
*/
@Parameter(defaultValue = "${project.basedir}/src/generated/resources")
protected File dataFormatOutDir;
/**
* The output directory for the generated data format java classes
*/
@Parameter(defaultValue = "${project.basedir}/src/generated/java")
protected File configurerSourceOutDir;
/**
* The output directory for the generated data format configurer resources
*/
@Parameter(defaultValue = "${project.basedir}/src/generated/resources")
protected File configurerResourceOutDir;
/**
* The output directory for the generated data format schema resources
*/
@Parameter(defaultValue = "${project.basedir}/src/generated/resources")
protected File schemaOutDir;
private final Map> sources = new HashMap<>();
@Inject
public PackageDataFormatMojo(MavenProjectHelper projectHelper, BuildContext buildContext) {
super(projectHelper, buildContext);
}
PackageDataFormatMojo(Log log, MavenProject project, MavenProjectHelper projectHelper,
File dataFormatOutDir, File configurerSourceOutDir,
File configurerResourceOutDir, File schemaOutDir,
BuildContext buildContext) {
this(projectHelper, buildContext);
setLog(log);
this.project = project;
this.dataFormatOutDir = dataFormatOutDir;
this.configurerSourceOutDir = configurerSourceOutDir;
this.configurerResourceOutDir = configurerResourceOutDir;
this.schemaOutDir = schemaOutDir;
}
/**
* 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...
*/
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
prepareDataFormat();
}
public int prepareDataFormat() throws MojoExecutionException {
Log log = getLog();
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 (!haveResourcesChanged(log, project, buildContext, "META-INF/services/org/apache/camel/dataformat")) {
return 0;
}
Map javaTypes = new HashMap<>();
StringBuilder buffer = new StringBuilder(256);
int count = 0;
File f = new File(project.getBasedir(), "target/classes");
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) {
File core = PackageHelper.findCamelCoreModelDirectory(project.getBasedir());
if (core != null) {
for (Map.Entry entry : javaTypes.entrySet()) {
String name = entry.getKey();
String javaType = entry.getValue();
String modelName = asModelName(name);
String json = PackageHelper.loadText(new File(
core,
"src/generated/resources/META-INF/org/apache/camel/model/dataformat/" + modelName
+ PackageHelper.JSON_SUFIX));
// any excluded properties
Class> clazz = loadClass(javaType);
Metadata metadata = clazz.getAnnotation(Metadata.class);
String included = "";
String excluded = "";
if (metadata != null) {
included = metadata.includeProperties();
excluded = metadata.excludeProperties();
}
final DataFormatModel dataFormatModel
= extractDataFormatModel(project, json, name, clazz, included, excluded);
SchemaHelper.addModelMetadata(dataFormatModel, project);
SchemaHelper.addModelMetadata(dataFormatModel, metadata);
if (!modelName.equals(name)) {
/* Prefer description from the clazz */
setDescriptionFromClass(clazz, dataFormatModel);
}
if (log.isDebugEnabled()) {
log.debug("Model: " + dataFormatModel);
}
String schema = JsonMapper.createParameterJsonSchema(dataFormatModel);
if (log.isDebugEnabled()) {
log.debug("JSON schema:\n" + schema);
}
// write this to the directory
Path out = schemaOutDir.toPath().resolve(schemaSubDirectory(dataFormatModel.getJavaType()))
.resolve(name + PackageHelper.JSON_SUFIX);
updateResource(schemaOutDir.toPath(),
schemaSubDirectory(dataFormatModel.getJavaType()) + "/" + name + PackageHelper.JSON_SUFIX,
schema);
if (log.isDebugEnabled()) {
log.debug("Generated " + out + " containing JSON schema for " + name + " data format");
}
String cn = javaType.substring(javaType.lastIndexOf('.') + 1);
String pn = javaType.substring(0, javaType.length() - cn.length() - 1);
Set names = dataFormatModel.getOptions().stream().map(DataFormatOptionModel::getName)
.collect(Collectors.toSet());
List options = parseConfigurationSource(project, javaType);
options.removeIf(o -> !names.contains(o.getName()));
options.stream().map(DataFormatOptionModel::getName).collect(Collectors.toList())
.forEach(names::remove);
names.removeAll(List.of("id"));
if (!names.isEmpty()) {
log.warn("Unmapped options: " + String.join(",", names));
}
updateResource(configurerSourceOutDir.toPath(),
pn.replace('.', '/') + "/" + cn + "Configurer.java",
generatePropertyConfigurer(pn, cn + "Configurer", cn, options));
updateResource(configurerResourceOutDir.toPath(),
"META-INF/services/org/apache/camel/configurer/" + name + "-dataformat",
generateMetaInfConfigurer(pn + "." + cn + "Configurer"));
}
} else {
throw new MojoExecutionException(
"Error finding core/camel-core/target/camel-core-model-" + project.getVersion()
+ ".jar file. Make sure camel-core has been built first.");
}
}
} catch (Exception e) {
throw new MojoExecutionException("Error loading dataformat model from camel-core. Reason: " + e, e);
}
if (count > 0) {
String names = Stream.of(buffer.toString().split(" ")).sorted().collect(Collectors.joining(" "));
String properties = createProperties(project, "dataFormats", names);
updateResource(camelMetaDir.toPath(), "dataformat.properties", properties);
log.info("Generated dataformat.properties containing " + count + " Camel "
+ (count > 1 ? "dataformats: " : "dataformat: ") + names);
} else {
log.debug(
"No META-INF/services/org/apache/camel/dataformat directory found. Are you sure you have created a Camel data format?");
}
return count;
}
private void setDescriptionFromClass(Class> clazz, final DataFormatModel dataFormatModel) {
javaClassSource(clazz.getName()).ifPresent(src -> {
String doc = src.getJavaDoc().getFullText();
if (doc != null) {
// need to sanitize the description first (we only want a
// summary)
doc = JavadocHelper.sanitizeDescription(doc, true);
// the javadoc may actually be empty, so only change the doc
// if we got something
if (!Strings.isNullOrEmpty(doc)) {
dataFormatModel.setDescription(doc);
}
}
});
}
private static DataFormatModel extractDataFormatModel(
MavenProject project, String json, String name, Class> javaType,
String includedProperties, String excludedProperties) {
EipModel def = JsonMapper.generateEipModel(json);
DataFormatModel model = new DataFormatModel();
model.setName(name);
model.setTitle(asModelTitle(name, def.getTitle()));
model.setDescription(def.getDescription());
model.setFirstVersion(asModelFirstVersion(name, def.getFirstVersion()));
model.setLabel(def.getLabel());
model.setDeprecated(def.isDeprecated());
model.setDeprecationNote(def.getDeprecationNote());
model.setDeprecatedSince(project.getProperties().getProperty("deprecatedSince"));
model.setJavaType(javaType.getCanonicalName());
model.setModelName(def.getName());
model.setModelJavaType(def.getJavaType());
model.setGroupId(project.getGroupId());
model.setArtifactId(project.getArtifactId());
model.setVersion(project.getVersion());
// grab level from annotation, pom.xml or default to stable
String level = project.getProperties().getProperty("supportLevel");
boolean experimental = ClassUtil.hasAnnotation("org.apache.camel.Experimental", javaType);
if (experimental) {
model.setSupportLevel(SupportLevel.Experimental);
} else if (level != null) {
model.setSupportLevel(SupportLevel.safeValueOf(level));
} else {
model.setSupportLevel(SupportLevelHelper.defaultSupportLevel(model.getFirstVersion(), model.getVersion()));
}
for (EipOptionModel opt : def.getOptions()) {
DataFormatOptionModel option = new DataFormatOptionModel();
if (excludedProperties.contains(opt.getName())) {
// skip excluded
continue;
}
if (!includedProperties.isEmpty() && !includedProperties.contains(opt.getName())) {
// skip if not included
continue;
}
option.setName(opt.getName());
option.setKind(opt.getKind());
option.setDisplayName(opt.getDisplayName());
option.setGroup(opt.getGroup());
option.setLabel(opt.getLabel());
option.setRequired(opt.isRequired());
option.setType(opt.getType());
option.setJavaType(opt.getJavaType());
option.setEnums(opt.getEnums());
option.setOneOfs(opt.getOneOfs());
option.setPrefix(opt.getPrefix());
option.setOptionalPrefix(opt.getOptionalPrefix());
option.setMultiValue(opt.isMultiValue());
option.setDeprecated(opt.isDeprecated());
option.setDeprecationNote(opt.getDeprecationNote());
option.setSecret(opt.isSecret());
option.setDefaultValue(opt.getDefaultValue());
option.setDefaultValueNote(opt.getDefaultValueNote());
option.setAsPredicate(opt.isAsPredicate());
option.setConfigurationClass(opt.getConfigurationClass());
option.setConfigurationField(opt.getConfigurationField());
option.setDescription(opt.getDescription());
if ("type".equals(option.getName()) && "bindy".equals(model.getModelName())) {
switch (name) {
case "bindyCsv":
option.setDefaultValue("Csv");
break;
case "bindyFixed":
option.setDefaultValue("Fixed");
break;
case "bindyKvp":
option.setDefaultValue("KeyValue");
break;
default:
}
}
if ("objectMapper".equals(option.getName()) && "johnzon".equals(name)) {
option.setDisplayName("Mapper");
option.setDescription("Lookup and use the existing Mapper with the given id.");
}
if ("objectMapper".equals(option.getName()) && "jsonb".equals(name)) {
option.setDisplayName("Jsonb instance");
option.setDescription("Lookup and use the existing Jsonb instance with the given id.");
}
if ("library".equals(option.getName()) && "json".equals(model.getModelName())) {
switch (name) {
case "gson":
option.setDefaultValue("Gson");
break;
case "jackson":
option.setDefaultValue("Jackson");
break;
case "johnzon":
option.setDefaultValue("Johnzon");
break;
case "jsonb":
option.setDefaultValue("JSON-B");
break;
case "json-fastson":
option.setDefaultValue("Fastjson");
break;
default:
}
}
model.addOption(option);
}
return model;
}
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.isEmpty()) {
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 = PackageHelper.loadText(file);
Map map = PackageHelper.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 ("gson".equals(name) || "jackson".equals(name) || "johnzon".equals(name)
|| "fastjson".equals(name) || "jsonb".equals(name)) {
return "json";
} else if ("bindyCsv".equals(name) || "bindyFixed".equals(name) || "bindyKvp".equals(name)) {
return "bindy";
} else if ("snakeYaml".equals(name)) {
return "yaml";
} else if ("avroJackson".equals(name)) {
return "avro";
} else if ("protobufJackson".equals(name)) {
return "protobuf";
}
return name;
}
private static String asModelFirstVersion(String name, String firstVersion) {
switch (name) {
case "gson":
return "2.10.0";
case "jackson":
return "2.0.0";
case "johnzon":
return "2.18.0";
case "jsonb":
return "3.7.0";
case "fastjson":
return "2.20.0";
case "avroJackson":
return "3.10.0";
case "protobufJackson":
return "3.10.0";
default:
return firstVersion;
}
}
private static String asModelTitle(String name, String title) {
// special for some data formats
if ("gson".equals(name)) {
return "JSON Gson";
} else if ("jackson".equals(name)) {
return "JSON Jackson";
} else if ("avroJackson".equals(name)) {
return "Avro Jackson";
} else if ("protobufJackson".equals(name)) {
return "Protobuf Jackson";
} else if ("johnzon".equals(name)) {
return "JSON Johnzon";
} else if ("jsonb".equals(name)) {
return "JSON JSON-B";
} else if ("fastjson".equals(name)) {
return "JSON Fastjson";
} else if ("bindyCsv".equals(name)) {
return "Bindy CSV";
} else if ("bindyFixed".equals(name)) {
return "Bindy Fixed Length";
} else if ("bindyKvp".equals(name)) {
return "Bindy Key Value Pair";
} else if ("snakeYaml".equals(name)) {
return "YAML SnakeYAML";
}
return title;
}
private static String schemaSubDirectory(String javaType) {
int idx = javaType.lastIndexOf('.');
String pckName = javaType.substring(0, idx);
return "META-INF/" + pckName.replace('.', '/');
}
private List parseConfigurationSource(MavenProject project, String className)
throws IOException {
final List answer = new ArrayList<>();
Optional optClazz = javaClassSource(className);
if (!optClazz.isPresent()) {
return Collections.emptyList();
}
JavaClassSource clazz = optClazz.get();
List> fields = clazz.getFields();
// filter out final or static fields
fields = fields.stream().filter(f -> !f.isFinal() && !f.isStatic()).collect(Collectors.toList());
fields.forEach(f -> {
String name = f.getName();
String javaType = f.getType().getQualifiedName();
if (f.getType().isArray()) {
javaType += "[]";
}
String setterName = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
MethodSource> setter = clazz.getMethod(setterName, javaType);
if (setter != null) {
DataFormatOptionModel model = new DataFormatOptionModel();
model.setName(name);
model.setJavaType(javaType);
answer.add(model);
}
});
if (clazz.getSuperType() != null) {
answer.addAll(parseConfigurationSource(project, clazz.getSuperType()));
}
return answer;
}
private Optional javaClassSource(String className) {
return sources.computeIfAbsent(className, this::doParseJavaClassSource);
}
private Optional doParseJavaClassSource(String className) {
try {
Path srcDir = project.getBasedir().toPath().resolve("src/main/java");
// Remove <.*> from className, as the string may contain generic types
Path file = srcDir.resolve(className.replaceAll("<.*>", "").replace('.', '/') + ".java");
if (!Files.isRegularFile(file)) {
return Optional.empty();
}
return Optional.of((JavaClassSource) Roaster.parse(file.toFile()));
} catch (IOException e) {
throw new RuntimeException("Unable to parse java class " + className, e);
}
}
public String generatePropertyConfigurer(String pn, String cn, String en, Collection options)
throws IOException {
Map ctx = new HashMap<>();
ctx.put("package", pn);
ctx.put("className", cn);
ctx.put("type", en);
ctx.put("options", options);
ctx.put("mojo", this);
return velocity("velocity/dataformat-property-configurer.vm", ctx);
}
public static String generateMetaInfConfigurer(String fqn) {
return "# " + GENERATED_MSG + NL + "class=" + fqn + NL;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy