com.hpe.adm.nga.sdk.generate.GenerateModels Maven / Gradle / Ivy
/*
* Copyright 2016-2023 Open Text.
*
* The only warranties for products and services of Open Text and
* its affiliates and licensors (“Open Text”) are as may be set forth
* in the express warranty statements accompanying such products and services.
* Nothing herein should be construed as constituting an additional warranty.
* Open Text shall not be liable for technical or editorial errors or
* omissions contained herein. The information contained herein is subject
* to change without notice.
*
* Except as specifically indicated otherwise, this document contains
* confidential information and a valid license is required for possession,
* use or copying. If this work is provided to the U.S. Government,
* consistent with FAR 12.211 and 12.212, Commercial Computer Software,
* Computer Software Documentation, and Technical Data for Commercial Items are
* licensed to the U.S. Government under vendor's standard commercial license.
*
* 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 com.hpe.adm.nga.sdk.generate;
import com.hpe.adm.nga.sdk.Octane;
import com.hpe.adm.nga.sdk.authentication.Authentication;
import com.hpe.adm.nga.sdk.entities.get.GetEntities;
import com.hpe.adm.nga.sdk.metadata.EntityMetadata;
import com.hpe.adm.nga.sdk.metadata.FieldMetadata;
import com.hpe.adm.nga.sdk.metadata.Metadata;
import com.hpe.adm.nga.sdk.metadata.features.Feature;
import com.hpe.adm.nga.sdk.metadata.features.RestFeature;
import com.hpe.adm.nga.sdk.metadata.features.SubTypesOfFeature;
import com.hpe.adm.nga.sdk.model.EmptyFieldModel;
import com.hpe.adm.nga.sdk.model.EntityModel;
import com.hpe.adm.nga.sdk.model.FieldModel;
import com.hpe.adm.nga.sdk.model.ReferenceFieldModel;
import com.hpe.adm.nga.sdk.model.StringFieldModel;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* The class that generates entities based on the metadata from the given ALM Octane server
* This class generates models based on the {@link com.hpe.adm.nga.sdk.model.TypedEntityModel},
* entity lists based on {@link com.hpe.adm.nga.sdk.entities.TypedEntityList} and Lists & Phases objects
* which represents those entities on the server and turns them into typed enums.
*
*
* The user that calls the generation must have the workspace member of the given workspace.
*
*
* UDFs are generated if they are part of the metadata for that workspace. That means that the generated
* entities should be able to be reused over different workspaces within the same shared space. However
* some business rules could cause different behaviour in different Workspaces. See the ALM Octane documentation
* for more information
*
*/
public class GenerateModels {
private final Template template, interfaceTemplate, entityListTemplate, phasesTemplate, listsTemplate, listInterfaceTemplate;
private final File modelDirectory, entitiesDirectory, enumsDirectory;
/**
* Initialise the class with the output directory. This should normally be in a project that would be
* imported into the main Java project
*
* @param outputDirectory Where all the generated files will be placed
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public GenerateModels(final File outputDirectory) {
final File packageDirectory = new File(outputDirectory, "/com/hpe/adm/nga/sdk");
modelDirectory = new File(packageDirectory, "model");
modelDirectory.mkdirs();
entitiesDirectory = new File(packageDirectory, "entities");
entitiesDirectory.mkdirs();
enumsDirectory = new File(packageDirectory, "enums");
enumsDirectory.mkdirs();
final VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty("resource.loader", "class");
velocityEngine.setProperty("class.resource.loader.description", "Velocity Classpath Resource Loader");
velocityEngine.setProperty(VelocityEngine.RUNTIME_LOG_LOGSYSTEM, new SLF4JLogChute());
velocityEngine.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.init();
template = velocityEngine.getTemplate("/EntityModel.vm");
interfaceTemplate = velocityEngine.getTemplate("/Entity.vm");
entityListTemplate = velocityEngine.getTemplate("/TypedEntityList.vm");
phasesTemplate = velocityEngine.getTemplate("/Phases.vm");
listsTemplate = velocityEngine.getTemplate("/Lists.vm");
listInterfaceTemplate = velocityEngine.getTemplate("/OctaneList.vm");
}
/**
* Run the actual generation
*
* @param authentication The authentication object
* @param server The server including the protocol and port
* @param sharedSpace The SS id
* @param workSpace The WS id
* @throws IOException A problem with the generation of the entities
*/
public void generate(Authentication authentication, String server, long sharedSpace, long workSpace) throws IOException {
generateListInterface();
final Octane octane = new Octane.Builder(authentication).sharedSpace(sharedSpace).workSpace(workSpace).Server(server).build();
final Metadata metadata = octane.metadata();
final Collection entityMetadata = metadata.entities().execute();
final Map logicalNameToListsMap = generateLists(octane);
final Set availablePhases = generatePhases(octane);
for (EntityMetadata entityMetadatum : entityMetadata) {
final String name = entityMetadatum.getName();
if (entityShouldNotBeGenerated(name)) continue;
final String interfaceName = GeneratorHelper.camelCaseFieldName(name) + "Entity";
final Collection fieldMetadata = generateEntity(metadata, entityMetadata, entityMetadatum, name, interfaceName, logicalNameToListsMap, availablePhases);
generateInterface(entityMetadatum, name, interfaceName);
generateEntityList(entityMetadatum, name, fieldMetadata);
}
}
private void generateListInterface() throws IOException {
final VelocityContext velocityContext = new VelocityContext();
final OutputStreamWriter fileWriter = new OutputStreamWriter(Files.newOutputStream(new File(enumsDirectory, "OctaneList.java").toPath()), StandardCharsets.UTF_8);
listInterfaceTemplate.merge(velocityContext, fileWriter);
fileWriter.close();
}
/**
* There are a few fields that cannot be generated due to inconsistencies. These could have special cases but it is simpler to exclude them
* from generation. If there is a problem then they can be checked on an individual basis
*
* @param name The entity that should be checked
* @return Whether this entity should be ignored and therefore not generated
*/
private boolean entityShouldNotBeGenerated(String name) {
/*
* @Since 15.1.20
* field_metadata is a special case in that it is used when defining UDFs. It causes problems in the entity generation due to the list node
* not having a reference. It is unlikely that this would be used by the SDK so is ignored for now. If this does cause an issue we could
* look into fixing this in the future
*/
if (name.startsWith("field_metadata")) {
return true;
}
return false;
}
private Map generateLists(Octane octane) throws IOException {
final Collection listNodes = new ArrayList<>();
GetEntities getListNodes =
octane.entityList("list_nodes").get().addFields("name", "list_root", "id", "logical_name").limit(1000);
int offset = 0;
Collection fetchedListNodes = getListNodes.offset(offset).execute();
while (fetchedListNodes.size() > 0) {
listNodes.addAll(fetchedListNodes);
offset += fetchedListNodes.size();
fetchedListNodes = getListNodes.offset(offset).execute();
}
final Map> mappedListNodes = new HashMap<>();
final Map logicalNameToNameMap = new HashMap<>();
listNodes.forEach(listNode -> {
final String rootId;
final FieldModel> listRootFieldModel = listNode.getValue("list_root");
final String name;
if (listRootFieldModel instanceof EmptyFieldModel) {
rootId = listNode.getId();
name = GeneratorHelper.camelCaseFieldName(GeneratorHelper.getJavaCompliantIdentifier(((StringFieldModel) listNode.getValue("name")).getValue()));
logicalNameToNameMap.put(((StringFieldModel) listNode.getValue("logical_name")).getValue(), getPackageForList(rootId).concat(".").concat(name));
} else {
final EntityModel list_rootValue = ((ReferenceFieldModel) listRootFieldModel).getValue();
rootId = list_rootValue.getId();
name = GeneratorHelper.getJavaCompliantIdentifier(((StringFieldModel) listNode.getValue("name")).getValue()).toUpperCase();
}
final List listHierarchy = mappedListNodes.computeIfAbsent(rootId, k -> new ArrayList<>());
final String[] listNodeInfo = {name, ((StringFieldModel) listNode.getValue("id")).getValue()};
if (listRootFieldModel instanceof EmptyFieldModel) {
listHierarchy.add(0, listNodeInfo);
} else {
listHierarchy.add(listNodeInfo);
}
});
for (final Map.Entry> entry : mappedListNodes.entrySet()) {
final String rootId = entry.getKey();
final List nodes = entry.getValue();
final String className = nodes.get(0)[0];
final Path listDirectoryPath = enumsDirectory.toPath().resolve(Paths.get("lists", getPackageForRootIdForList(rootId).split("\\.")));
final File listDirectoryPathFile = listDirectoryPath.toFile();
//noinspection ResultOfMethodCallIgnored
listDirectoryPathFile.mkdirs();
final File listFile = new File(listDirectoryPathFile, className + ".java");
final VelocityContext velocityContext = new VelocityContext();
velocityContext.put("listItemsSet", entry);
velocityContext.put("className", className);
velocityContext.put("packageName", getPackageForList(rootId));
final OutputStreamWriter fileWriter = new OutputStreamWriter(Files.newOutputStream(listFile.toPath()), StandardCharsets.UTF_8);
listsTemplate.merge(velocityContext, fileWriter);
fileWriter.close();
}
return logicalNameToNameMap;
}
private String getPackageForList(final String rootId) {
return "com.hpe.adm.nga.sdk.enums.lists.".concat(getPackageForRootIdForList(rootId));
}
private String getPackageForRootIdForList(String rootId) {
final String[] splitRootIds = rootId.split("\\.");
final StringBuilder packageStringBuilder = new StringBuilder();
for (int i = 0; i < splitRootIds.length - 1; i++) {
final String splitRootId = getPackagePrefixForLists(splitRootIds[i]);
packageStringBuilder.append(splitRootId).append(".");
}
packageStringBuilder.append(getPackagePrefixForLists(splitRootIds[splitRootIds.length - 1]));
return packageStringBuilder.toString();
}
private String getPackagePrefixForLists(String packageName) {
return Character.isJavaIdentifierStart(packageName.charAt(0)) ? packageName : "_" + packageName;
}
private Set generatePhases(Octane octane) throws IOException {
final Map> phaseMap = new HashMap<>();
final Collection phases = octane.entityList("phases").get().addFields("id", "name", "entity").execute();
phases.forEach(phase -> {
final Set phaseValueSet = new HashSet<>();
phaseValueSet.add(new String[]{phase.getId(), createIdentifier(((StringFieldModel) phase.getValue("name")).getValue())});
phaseMap.merge(GeneratorHelper.camelCaseFieldName(((StringFieldModel) phase.getValue("entity")).getValue(), true), phaseValueSet, (existingValues, newValues) -> {
existingValues.addAll(newValues);
return existingValues;
});
});
final VelocityContext velocityContext = new VelocityContext();
velocityContext.put("phaseMap", phaseMap);
final OutputStreamWriter fileWriter = new OutputStreamWriter(Files.newOutputStream(new File(enumsDirectory, "Phases.java").toPath()), StandardCharsets.UTF_8);
phasesTemplate.merge(velocityContext, fileWriter);
fileWriter.close();
return phaseMap.keySet();
}
private Collection generateEntity(Metadata metadata, Collection entityMetadata, EntityMetadata entityMetadatum, String name, String interfaceName, Map logicalNameToListsMap, Set availablePhases) throws IOException {
final Collection fieldMetadata = sanitiseMetaData(metadata.fields(name).execute());
final TreeMap> collectedReferences = fieldMetadata.stream()
.filter(FieldMetadata::isRequired)
.collect(Collectors.toMap(FieldMetadata::getName, fieldMetadata1 -> {
final List references = new ArrayList<>();
final String className = GeneratorHelper.camelCaseFieldName(entityMetadatum.getName());
if (fieldMetadata1.getName().equals("phase") && availablePhases.contains(className)) {
references.add("com.hpe.adm.nga.sdk.enums.Phases." + className + "Phase");
} else if (fieldMetadata1.getFieldType() == FieldMetadata.FieldType.Reference) {
if ((!entityMetadatum.getName().equals("list_node")) && (fieldMetadata1.getFieldTypedata().getTargets()[0].getType().equals("list_node"))) {
final String listName = logicalNameToListsMap.getOrDefault(fieldMetadata1.getFieldTypedata().getTargets()[0].logicalName(), "OctaneList");
if (fieldMetadata1.getFieldTypedata().isMultiple()) {
references.add("java.util.Collection<" + listName + ">");
} else {
references.add(listName);
}
} else {
final GeneratorHelper.ReferenceMetadata referenceMetadata = GeneratorHelper.getAllowedSuperTypesForReference(fieldMetadata1, entityMetadata);
if (fieldMetadata1.getFieldTypedata().isMultiple()) {
assert referenceMetadata != null;
references.add(referenceMetadata.getReferenceClassForSignature());
} else {
assert referenceMetadata != null;
if (referenceMetadata.hasTypedReturn()) {
references.addAll(referenceMetadata.getReferenceTypes()
.stream()
.map(type -> GeneratorHelper.camelCaseFieldName(type).concat("EntityModel"))
.collect(Collectors.toSet()));
}
if (referenceMetadata.hasNonTypedReturn()) {
references.add("EntityModel");
}
}
}
} else {
references.add(GeneratorHelper.getFieldTypeAsJava(fieldMetadata1.getFieldType()));
}
return references;
}, (strings, strings2) -> {
throw new IllegalStateException("problem merging map");
}, TreeMap::new));
final Set> requiredFields = new HashSet<>();
if (!collectedReferences.isEmpty()) {
expandCollectedReferences(collectedReferences, new int[collectedReferences.size()], 0, requiredFields);
}
final VelocityContext velocityContext = new VelocityContext();
velocityContext.put("interfaceName", interfaceName);
velocityContext.put("entityMetadata", entityMetadatum);
velocityContext.put("fieldMetadata", fieldMetadata);
velocityContext.put("logicalNameToListsMap", logicalNameToListsMap);
velocityContext.put("entityMetadataCollection", entityMetadata);
velocityContext.put("GeneratorHelper", GeneratorHelper.class);
velocityContext.put("entityMetadataWrapper", GeneratorHelper.entityMetadataWrapper(entityMetadatum));
velocityContext.put("availablePhases", availablePhases);
velocityContext.put("requiredFields", requiredFields);
final OutputStreamWriter fileWriter = new OutputStreamWriter(Files.newOutputStream(new File(modelDirectory, GeneratorHelper.camelCaseFieldName(name) + "EntityModel.java").toPath()), StandardCharsets.UTF_8);
template.merge(velocityContext, fileWriter);
fileWriter.close();
return fieldMetadata;
}
private Collection sanitiseMetaData(Collection fieldMetadataCollection) {
return fieldMetadataCollection.stream()
// filter out fields that have references without a target - like public to protected
.filter(fieldMetadata -> fieldMetadata.getFieldType() != FieldMetadata.FieldType.Reference || fieldMetadata.getFieldTypedata().getTargets() != null)
// filter out fields that have references to field_metadata
.filter(fieldMetadata -> fieldMetadata.getFieldType() != FieldMetadata.FieldType.Reference || !fieldMetadata.getFieldTypedata().getTargets()[0].getType().startsWith("field_metadata"))
// filter out the id field, it is created automatically in the TypedEntityModel
.filter(fieldMetadata -> !fieldMetadata.getName().equals("id"))
// filter out the type field, it is created automatically in the TypedEntityModel, ci_parameter has a field called type that overrides id
.filter(fieldMetadata -> !fieldMetadata.getName().equals("type"))
.collect(Collectors.toList());
}
private void expandCollectedReferences(final TreeMap> collectedReferences, final int[] positions, final int pointer, final Set> output) {
final String[] keyArray = collectedReferences.keySet().toArray(new String[0]);
final String o = keyArray[pointer];
for (int i = 0; i < collectedReferences.get(o).size(); ++i) {
if (pointer == positions.length - 1) {
final List outputLine = new ArrayList<>(positions.length);
for (int j = 0; j < positions.length; ++j) {
outputLine.add(new String[]{keyArray[j], collectedReferences.get(keyArray[j]).get(positions[j])});
}
output.add(outputLine);
} else {
expandCollectedReferences(collectedReferences, positions, pointer + 1, output);
}
positions[pointer]++;
}
positions[pointer] = 0;
}
private void generateInterface(EntityMetadata entityMetadatum, String name, String interfaceName) throws IOException {
// interface
final VelocityContext interfaceVelocityContext = new VelocityContext();
final Optional subTypeOfFeature = entityMetadatum.features().stream().filter(feature -> feature instanceof SubTypesOfFeature).findAny();
interfaceVelocityContext.put("interfaceName", interfaceName);
interfaceVelocityContext.put("superInterfaceName",
(subTypeOfFeature.map(feature -> GeneratorHelper.camelCaseFieldName(((SubTypesOfFeature) feature).getType())).orElse("")) + "Entity");
final OutputStreamWriter interfaceFileWriter = new OutputStreamWriter(Files.newOutputStream(new File(modelDirectory, GeneratorHelper.camelCaseFieldName(name) + "Entity.java").toPath()), StandardCharsets.UTF_8);
interfaceTemplate.merge(interfaceVelocityContext, interfaceFileWriter);
interfaceFileWriter.close();
}
private void generateEntityList(EntityMetadata entityMetadatum, String name, Collection fieldMetadata) throws IOException {
// entityList
final Optional hasRestFeature = entityMetadatum.features().stream()
.filter(feature -> feature instanceof RestFeature)
.findFirst();
// if not then something is wrong!
if (hasRestFeature.isPresent()) {
final RestFeature restFeature = (RestFeature) hasRestFeature.get();
final VelocityContext entityListVelocityContext = new VelocityContext();
entityListVelocityContext.put("helper", GeneratorHelper.class);
entityListVelocityContext.put("type", GeneratorHelper.camelCaseFieldName(name));
entityListVelocityContext.put("url", restFeature.getUrl());
entityListVelocityContext.put("availableFields", fieldMetadata.stream().map(FieldMetadata::getName).collect(Collectors.toList()));
entityListVelocityContext.put("sortableFields", fieldMetadata.stream().filter(FieldMetadata::isSortable).map(FieldMetadata::getName).collect(Collectors.toList()));
final String[] restFeatureMethods = restFeature.getMethods();
for (String restFeatureMethod : restFeatureMethods) {
switch (restFeatureMethod) {
case "GET":
entityListVelocityContext.put("hasGet", true);
break;
case "POST":
entityListVelocityContext.put("hasCreate", true);
break;
case "PUT":
entityListVelocityContext.put("hasUpdate", true);
break;
case "DELETE":
entityListVelocityContext.put("hasDelete", true);
break;
}
}
// add test script support for test_manual or gherkin tests only
if (name.equals("test_manual") || name.equals("gherkin_tests")) {
entityListVelocityContext.put("hasTestScript", true);
}
final OutputStreamWriter entityListFileWriter = new OutputStreamWriter(Files.newOutputStream(new File(entitiesDirectory, GeneratorHelper.camelCaseFieldName(name) + "EntityList.java").toPath()), StandardCharsets.UTF_8);
entityListTemplate.merge(entityListVelocityContext, entityListFileWriter);
entityListFileWriter.close();
}
}
private String createIdentifier(final String phaseValue) {
if (phaseValue == null || phaseValue.length() == 0) {
return "_";
}
final char[] c = phaseValue.toCharArray();
if (!Character.isJavaIdentifierStart(c[0])) {
c[0] = '_';
}
for (int i = 1; i < c.length; i++) {
if (!Character.isJavaIdentifierPart(c[i])) {
c[i] = '_';
}
}
return new String(c).toUpperCase();
}
}