blob: 412fe649937172a418d467aeb99666a47832a98f [file] [log] [blame]
/**
* 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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.camel.component.salesforce.api.dto.AbstractSObjectBase;
import org.apache.camel.component.salesforce.api.dto.PickListValue;
import org.apache.camel.component.salesforce.api.dto.SObjectDescription;
import org.apache.camel.component.salesforce.api.dto.SObjectField;
import org.apache.camel.component.salesforce.internal.client.RestClient;
import org.apache.camel.util.IntrospectionSupport;
import org.apache.camel.util.StringHelper;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
/**
* Goal to generate DTOs for Salesforce SObjects
*/
@Mojo(name = "generate", requiresProject = false, defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class GenerateMojo extends AbstractSalesforceMojo {
public class GeneratorUtility {
private Stack<String> stack;
private final Map<String, AtomicInteger> varNames = new HashMap<>();
public String current() {
return stack.peek();
}
public String enumTypeName(final String name) {
return (name.endsWith("__c") ? name.substring(0, name.length() - 3) : name) + "Enum";
}
public List<SObjectField> externalIdsOf(final String name) {
return descriptions.externalIdsOf(name);
}
public String getEnumConstant(final String value) {
// TODO add support for supplementary characters
final StringBuilder result = new StringBuilder();
boolean changed = false;
if (!Character.isJavaIdentifierStart(value.charAt(0))) {
result.append("_");
changed = true;
}
for (final char c : value.toCharArray()) {
if (Character.isJavaIdentifierPart(c)) {
result.append(c);
} else {
// replace non Java identifier character with '_'
result.append('_');
changed = true;
}
}
return changed ? result.toString().toUpperCase() : value.toUpperCase();
}
public String getFieldType(final SObjectDescription description, final SObjectField field) {
// check if this is a picklist
if (isPicklist(field)) {
if (Boolean.TRUE.equals(useStringsForPicklists)) {
return String.class.getName();
}
// use a pick list enum, which will be created after generating
// the SObject class
return description.getName() + "_" + enumTypeName(field.getName());
} else if (isMultiSelectPicklist(field)) {
if (useStringsForPicklists) {
return String.class.getName() + "[]";
}
// use a pick list enum array, enum will be created after
// generating the SObject class
return description.getName() + "_" + enumTypeName(field.getName()) + "[]";
} else {
// map field to Java type
final String soapType = field.getSoapType();
final String lookupType = soapType.substring(soapType.indexOf(':') + 1);
final String type = types.get(lookupType);
if (type == null) {
getLog().warn(String.format("Unsupported field type `%s` in field `%s` of object `%s`", soapType,
field.getName(), description.getName()));
getLog().debug("Currently known types:\n " + types.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("\n")));
}
return type;
}
}
public String getLookupRelationshipName(final SObjectField field) {
return StringHelper.notEmpty(field.getRelationshipName(), "relationshipName", field.getName());
}
public List<PickListValue> getUniqueValues(final SObjectField field) {
if (field.getPicklistValues().isEmpty()) {
return field.getPicklistValues();
}
final List<PickListValue> result = new ArrayList<>();
final Set<String> literals = new HashSet<>();
for (final PickListValue listValue : field.getPicklistValues()) {
final String value = listValue.getValue();
if (!literals.contains(value)) {
literals.add(value);
result.add(listValue);
}
}
literals.clear();
Collections.sort(result, (o1, o2) -> o1.getValue().compareTo(o2.getValue()));
return result;
}
public boolean hasMultiSelectPicklists(final SObjectDescription desc) {
for (final SObjectField field : desc.getFields()) {
if (isMultiSelectPicklist(field)) {
return true;
}
}
return false;
}
public boolean hasPicklists(final SObjectDescription desc) {
for (final SObjectField field : desc.getFields()) {
if (isPicklist(field)) {
return true;
}
}
return false;
}
public boolean includeList(final List<?> list, final String propertyName) {
return !list.isEmpty() && !BLACKLISTED_PROPERTIES.contains(propertyName);
}
public boolean isBlobField(final SObjectField field) {
final String soapType = field.getSoapType();
return BASE64BINARY.equals(soapType.substring(soapType.indexOf(':') + 1));
}
public boolean isExternalId(final SObjectField field) {
return field.isExternalId();
}
public boolean isLookup(final SObjectField field) {
return "reference".equals(field.getType());
}
public boolean isMultiSelectPicklist(final SObjectField field) {
return MULTIPICKLIST.equals(field.getType());
}
public boolean isPicklist(final SObjectField field) {
return PICKLIST.equals(field.getType());
}
public boolean isPrimitiveOrBoxed(final Object object) {
final Class<?> clazz = object.getClass();
final boolean isWholeNumberWrapper = Byte.class.equals(clazz) || Short.class.equals(clazz)
|| Integer.class.equals(clazz) || Long.class.equals(clazz);
final boolean isFloatingPointWrapper = Double.class.equals(clazz) || Float.class.equals(clazz);
final boolean isWrapper = isWholeNumberWrapper || isFloatingPointWrapper || Boolean.class.equals(clazz)
|| Character.class.equals(clazz);
final boolean isPrimitive = clazz.isPrimitive();
return isPrimitive || isWrapper;
}
public boolean notBaseField(final String name) {
return !BASE_FIELDS.contains(name);
}
public boolean notNull(final Object val) {
return val != null;
}
public void pop() {
stack.pop();
}
public Set<Map.Entry<String, Object>> propertiesOf(final Object object) {
final Map<String, Object> properties = new HashMap<>();
IntrospectionSupport.getProperties(object, properties, null, false);
return properties.entrySet().stream()
.collect(Collectors.toMap(e -> StringUtils.capitalize(e.getKey()), Map.Entry::getValue)).entrySet();
}
public void push(final String additional) {
stack.push(additional);
}
public void start(final String initial) {
stack = new Stack<>();
stack.push(initial);
varNames.clear();
}
public String variableName(final String given) {
final String base = StringUtils.uncapitalize(given);
AtomicInteger counter = varNames.get(base);
if (counter == null) {
counter = new AtomicInteger(0);
varNames.put(base, counter);
}
return base + counter.incrementAndGet();
}
}
public static final Map<String, String> DEFAULT_TYPES = defineLookupMap();
private static final Set<String> BASE_FIELDS = defineBaseFields();
private static final String BASE64BINARY = "base64Binary";
private static final List<String> BLACKLISTED_PROPERTIES = Arrays.asList("PicklistValues", "ChildRelationships");
private static final String JAVA_EXT = ".java";
// used for velocity logging, to avoid creating velocity.log
private static final Logger LOG = Logger.getLogger(GenerateMojo.class.getName());
private static final String MULTIPICKLIST = "multipicklist";
private static final String PACKAGE_NAME_PATTERN = "(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.)+\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";
private static final String PICKLIST = "picklist";
private static final String SOBJECT_LOOKUP_VM = "/sobject-lookup.vm";
private static final String SOBJECT_PICKLIST_VM = "/sobject-picklist.vm";
private static final String SOBJECT_POJO_OPTIONAL_VM = "/sobject-pojo-optional.vm";
private static final String SOBJECT_POJO_VM = "/sobject-pojo.vm";
private static final String SOBJECT_QUERY_RECORDS_OPTIONAL_VM = "/sobject-query-records-optional.vm";
private static final String SOBJECT_QUERY_RECORDS_VM = "/sobject-query-records.vm";
private static final String UTF_8 = "UTF-8";
@Parameter
Map<String, String> customTypes;
ObjectDescriptions descriptions;
VelocityEngine engine;
/**
* Include Salesforce SObjects that match pattern.
*/
@Parameter(property = "camelSalesforce.includePattern")
String includePattern;
/**
* Location of generated DTO files, defaults to
* target/generated-sources/camel-salesforce.
*/
@Parameter(property = "camelSalesforce.outputDirectory",
defaultValue = "${project.build.directory}/generated-sources/camel-salesforce")
File outputDirectory;
/**
* Java package name for generated DTOs.
*/
@Parameter(property = "camelSalesforce.packageName", defaultValue = "org.apache.camel.salesforce.dto")
String packageName;
/**
* Exclude Salesforce SObjects that match pattern.
*/
@Parameter(property = "camelSalesforce.excludePattern")
private String excludePattern;
/**
* Do NOT generate DTOs for these Salesforce SObjects.
*/
@Parameter
private String[] excludes;
/**
* Names of Salesforce SObject for which DTOs must be generated.
*/
@Parameter
private String[] includes;
private final Map<String, String> types = new HashMap<>(DEFAULT_TYPES);
@Parameter(property = "camelSalesforce.useOptionals", defaultValue = "false")
private boolean useOptionals;
@Parameter(property = "camelSalesforce.useStringsForPicklists", defaultValue = "false")
private Boolean useStringsForPicklists;
void processDescription(final File pkgDir, final SObjectDescription description, final GeneratorUtility utility,
final String generatedDate) throws IOException {
// generate a source file for SObject
final VelocityContext context = new VelocityContext();
context.put("packageName", packageName);
context.put("utility", utility);
context.put("esc", StringEscapeUtils.class);
context.put("desc", description);
context.put("generatedDate", generatedDate);
context.put("useStringsForPicklists", useStringsForPicklists);
final String pojoFileName = description.getName() + JAVA_EXT;
final File pojoFile = new File(pkgDir, pojoFileName);
context.put("descriptions", descriptions);
try (final Writer writer = new OutputStreamWriter(new FileOutputStream(pojoFile), StandardCharsets.UTF_8)) {
final Template pojoTemplate = engine.getTemplate(SOBJECT_POJO_VM, UTF_8);
pojoTemplate.merge(context, writer);
}
if (useOptionals) {
final String optionalFileName = description.getName() + "Optional" + JAVA_EXT;
final File optionalFile = new File(pkgDir, optionalFileName);
try (final Writer writer = new OutputStreamWriter(new FileOutputStream(optionalFile),
StandardCharsets.UTF_8)) {
final Template optionalTemplate = engine.getTemplate(SOBJECT_POJO_OPTIONAL_VM, UTF_8);
optionalTemplate.merge(context, writer);
}
}
// generate ExternalIds Lookup class for all lookup fields that point to
// an Object that has at least one externalId
final Set<String> generatedLookupObjects = new HashSet<>();
for (final SObjectField field : description.getFields()) {
if (!utility.isLookup(field)) {
continue;
}
for (final String reference : field.getReferenceTo()) {
final List<SObjectField> externalIds = descriptions.externalIdsOf(reference);
for (final SObjectField externalId : externalIds) {
final String lookupClassName = reference + "_Lookup";
if (generatedLookupObjects.contains(lookupClassName)) {
continue;
}
generatedLookupObjects.add(lookupClassName);
final String lookupClassFileName = lookupClassName + JAVA_EXT;
final File lookupClassFile = new File(pkgDir, lookupClassFileName);
context.put("field", externalId);
context.put("lookupRelationshipName", field.getRelationshipName());
context.put("lookupType", lookupClassName);
context.put("externalIdsList", externalIds);
context.put("lookupClassName", lookupClassName);
try (final Writer writer = new OutputStreamWriter(new FileOutputStream(lookupClassFile),
StandardCharsets.UTF_8)) {
final Template lookupClassTemplate = engine.getTemplate(SOBJECT_LOOKUP_VM, UTF_8);
lookupClassTemplate.merge(context, writer);
}
}
}
}
// write required Enumerations for any picklists
for (final SObjectField field : description.getFields()) {
if (utility.isPicklist(field) || utility.isMultiSelectPicklist(field)) {
final String enumName = description.getName() + "_" + utility.enumTypeName(field.getName());
final String enumFileName = enumName + JAVA_EXT;
final File enumFile = new File(pkgDir, enumFileName);
context.put("field", field);
context.put("enumName", enumName);
final Template enumTemplate = engine.getTemplate(SOBJECT_PICKLIST_VM, UTF_8);
try (final Writer writer = new OutputStreamWriter(new FileOutputStream(enumFile),
StandardCharsets.UTF_8)) {
enumTemplate.merge(context, writer);
}
}
}
// write the QueryRecords class
final String queryRecordsFileName = "QueryRecords" + description.getName() + JAVA_EXT;
final File queryRecordsFile = new File(pkgDir, queryRecordsFileName);
final Template queryTemplate = engine.getTemplate(SOBJECT_QUERY_RECORDS_VM, UTF_8);
try (final Writer writer = new OutputStreamWriter(new FileOutputStream(queryRecordsFile),
StandardCharsets.UTF_8)) {
queryTemplate.merge(context, writer);
}
if (useOptionals) {
// write the QueryRecords Optional class
final String queryRecordsOptionalFileName = "QueryRecords" + description.getName() + "Optional" + JAVA_EXT;
final File queryRecordsOptionalFile = new File(pkgDir, queryRecordsOptionalFileName);
final Template queryRecordsOptionalTemplate = engine.getTemplate(SOBJECT_QUERY_RECORDS_OPTIONAL_VM, UTF_8);
try (final Writer writer = new OutputStreamWriter(new FileOutputStream(queryRecordsOptionalFile),
StandardCharsets.UTF_8)) {
queryRecordsOptionalTemplate.merge(context, writer);
}
}
}
@Override
protected void executeWithClient(final RestClient client) throws MojoExecutionException {
descriptions = new ObjectDescriptions(client, getResponseTimeout(), includes, includePattern, excludes,
excludePattern, getLog());
engine = createVelocityEngine();
// make sure we can load both templates
if (!engine.resourceExists(SOBJECT_POJO_VM) || !engine.resourceExists(SOBJECT_QUERY_RECORDS_VM)
|| !engine.resourceExists(SOBJECT_POJO_OPTIONAL_VM)
|| !engine.resourceExists(SOBJECT_QUERY_RECORDS_OPTIONAL_VM)) {
throw new MojoExecutionException("Velocity templates not found");
}
// create package directory
// validate package name
if (!packageName.matches(PACKAGE_NAME_PATTERN)) {
throw new MojoExecutionException("Invalid package name " + packageName);
}
if (outputDirectory.getAbsolutePath().contains("$")) {
outputDirectory = new File("generated-sources/camel-salesforce");
}
final File pkgDir = new File(outputDirectory, packageName.trim().replace('.', File.separatorChar));
if (!pkgDir.exists()) {
if (!pkgDir.mkdirs()) {
throw new MojoExecutionException("Unable to create " + pkgDir);
}
}
getLog().info("Generating Java Classes...");
// generate POJOs for every object description
final GeneratorUtility utility = new GeneratorUtility();
// should we provide a flag to control timestamp generation?
final String generatedDate = new Date().toString();
for (final SObjectDescription description : descriptions.fetched()) {
if (Defaults.IGNORED_OBJECTS.contains(description.getName())) {
continue;
}
try {
processDescription(pkgDir, description, utility, generatedDate);
} catch (final IOException e) {
throw new MojoExecutionException("Unable to generate source files for: " + description.getName(), e);
}
}
getLog().info(String.format("Successfully generated %s Java Classes", descriptions.count() * 2));
}
@Override
protected void setup() {
if (customTypes != null) {
types.putAll(customTypes);
}
}
static VelocityEngine createVelocityEngine() {
// initialize velocity to load resources from class loader and use Log4J
final Properties velocityProperties = new Properties();
velocityProperties.setProperty(RuntimeConstants.RESOURCE_LOADER, "cloader");
velocityProperties.setProperty("cloader.resource.loader.class", ClasspathResourceLoader.class.getName());
velocityProperties.setProperty(RuntimeConstants.RUNTIME_LOG_NAME, LOG.getName());
final VelocityEngine engine = new VelocityEngine(velocityProperties);
return engine;
}
private static Set<String> defineBaseFields() {
final Set<String> baseFields = new HashSet<>();
for (final Field field : AbstractSObjectBase.class.getDeclaredFields()) {
baseFields.add(field.getName());
}
return baseFields;
}
private static Map<String, String> defineLookupMap() {
// create a type map
// using JAXB mapping, for the most part
// mapping for tns:ID SOAPtype
final String[][] typeMap = new String[][] {//
{"ID", "String"}, //
{"string", "String"}, //
{"integer", "java.math.BigInteger"}, //
{"int", "Integer"}, //
{"long", "Long"}, //
{"short", "Short"}, //
{"decimal", "java.math.BigDecimal"}, //
{"float", "Float"}, //
{"double", "Double"}, //
{"boolean", "Boolean"}, //
{"byte", "Byte"}, //
// the blob base64Binary type is mapped to String URL for retrieving
// the blob
{"base64Binary", "String"}, //
{"unsignedInt", "Long"}, //
{"unsignedShort", "Integer"}, //
{"unsignedByte", "Short"}, //
{"dateTime", "java.time.ZonedDateTime"}, //
{"time", "java.time.OffsetTime"}, //
{"date", "java.time.LocalDate"}, //
{"g", "java.time.ZonedDateTime"}, //
// Salesforce maps any types like string, picklist, reference, etc.
// to string
{"anyType", "String"}, //
{"address", "org.apache.camel.component.salesforce.api.dto.Address"}, //
{"location", "org.apache.camel.component.salesforce.api.dto.GeoLocation"}, //
{"RelationshipReferenceTo", "String"}//
};
final Map<String, String> lookupMap = new HashMap<>();
for (final String[] entry : typeMap) {
lookupMap.put(entry[0], entry[1]);
}
return Collections.unmodifiableMap(lookupMap);
}
}