Skip to content

Instantly share code, notes, and snippets.

@rismehta
Created June 19, 2024 13:31
Show Gist options
  • Save rismehta/fc9512317b26b60a16a839255977197c to your computer and use it in GitHub Desktop.
Save rismehta/fc9512317b26b60a16a839255977197c to your computer and use it in GitHub Desktop.
Core Component Form Generator
import com.adobe.aemds.guide.utils.GuideConstants;
import com.day.cq.wcm.api.components.Component;
import com.day.cq.wcm.api.components.ComponentManager;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.validation.constraints.Null;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class generates Core Component forms.
*
* Note: Layout layer specific properties (columnCount, columnClassNames, gridClassNames) are not supported.
*
* Note: All Server side propeties like submit action , prefill service etc which is not part of JSON would not be present when form is created
*/
public class CoreComponentFormGenerator {
private static final Logger logger = LoggerFactory.getLogger(CoreComponentFormGenerator.class);
private final ResourceResolver resourceResolver;
private final JsonObject form;
private final Map<String, String> resourceMappings = new HashMap<>();
private final boolean createResources;
public CoreComponentFormGenerator(@Nullable ResourceResolver resourceResolver, @Nonnull String crisprJsonString, @Nullable Map<String, String> resourceMappings, boolean createResources) {
this.resourceResolver = resourceResolver;
this.form = parseJsonString(crisprJsonString);
this.createResources = createResources;
if (resourceMappings != null) {
this.resourceMappings.putAll(resourceMappings);
} else {
initializeResourceMappings();
}
}
public CoreComponentFormGenerator(@Nullable ResourceResolver resourceResolver, @Nonnull String crisprJsonString, @Nullable Map<String, String> resourceMappings) {
this(resourceResolver, crisprJsonString, resourceMappings, true);
}
public CoreComponentFormGenerator(@Nonnull String crisprJsonString) {
this(null, crisprJsonString, null, false);
}
/**
* Creates a JsonObjectBuilder and adds a default property.
*
* @return a JsonObjectBuilder with the default property included.
*/
private static JsonObjectBuilder createObjectBuilderWithDefaults() {
JsonObjectBuilder builder = Json.createObjectBuilder();
builder.add(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
return builder;
}
private JsonObject parseJsonString(String crisprJsonString) {
try (JsonReader jsonReader = Json.createReader(new StringReader(crisprJsonString))) {
return jsonReader.readObject();
} catch (Exception ex) {
logger.error("Failed to parse JSON string", ex);
throw new IllegalArgumentException("Invalid JSON string provided", ex);
}
}
private void initializeResourceMappings() {
final String productComponentPrefix = "core/fd/components/form";
resourceMappings.put("plain-text", productComponentPrefix + "/text/v1/text");
resourceMappings.put("multiline-input", productComponentPrefix + "/text/v1/text");
resourceMappings.put("text-input", productComponentPrefix + "/textinput/v1/textinput");
resourceMappings.put("panel", productComponentPrefix + "/panelcontainer/v1/panelcontainer");
resourceMappings.put("file-input", productComponentPrefix + "/fileinput/v1/fileinput");
resourceMappings.put("checkbox-group", productComponentPrefix + "/checkboxgroup/v1/checkboxgroup");
resourceMappings.put("checkbox", productComponentPrefix + "/checkbox/v1/checkbox");
resourceMappings.put("radio-group", productComponentPrefix + "/radiobutton/v1/radiobutton");
resourceMappings.put("button", productComponentPrefix + "/button/v1/button");
resourceMappings.put("drop-down", productComponentPrefix + "/dropdown/v1/dropdown");
resourceMappings.put("date-input", productComponentPrefix + "/datepicker/v1/datepicker");
resourceMappings.put("number-input", productComponentPrefix + "/numberinput/v1/numberinput");
resourceMappings.put("email", productComponentPrefix + "/emailinput/v1/emailinput");
//resourceMappings.put("sign", productComponentPrefix + "/adobesignblock");
resourceMappings.put("captcha", productComponentPrefix + "/recaptcha/v1/recaptcha");
resourceMappings.put("submit", productComponentPrefix + "/actions/submit/v1/submit");
resourceMappings.put("form", productComponentPrefix + "/container/v2/container");
}
/**
* This method is responsible for generating a form as a JSON string. The generated JSON string can be used in the content tree inside the form editor.
*
* @return A string representation of the generated form in JSON format.
*
* @throws Exception If an error occurs during the form generation process, an exception is thrown. This could be due to issues like invalid form configurations, inability to access necessary resources, etc.
*/
public String generateFormContainer() throws Exception {
return generateFormContainer(null);
}
/**
* This method generates a form with the given parameters.
* It creates an AEM page and a DAM asset using the provided form name, template path, page resource type, and form container resource type.
*
* @param formName The name of the form to be created. This will be used as the name of the AEM page and DAM asset.
* @param templatePath The path to the template to be used for creating the AEM page.
* @param pageResourceType The resource type to be used for the AEM page.
* @param formContainerResourceType The resource type to be used for the form container.
*
* @throws Exception If an error occurs during the form generation process, an exception is thrown. This could be due to issues like invalid form configurations, inability to access necessary resources, etc.
*/
public void generateForm(@Nonnull String formName, @Nonnull String templatePath, @Nonnull String pageResourceType, @Nonnull String formContainerResourceType) throws Exception {
Resource guideContainerResource = createAdaptiveFormPageAndReturnFormContainerResource(formName, pageResourceType, templatePath, formContainerResourceType);
if (guideContainerResource != null) {
generateFormContainer(guideContainerResource.getPath());
}
}
/**
* This method creates an AEM page and a DAM asset.
* It takes four parameters: formName, pageResourceType, templatePath, and formContainerResourceType.
* It uses these parameters to create an AEM page and a DAM asset.
* If the ResourceResolver is null or if the parent resources cannot be found, it logs an error message.
*/
private Resource createAdaptiveFormPageAndReturnFormContainerResource(@Nonnull String formName, @Nonnull String pageResourceType, @Nonnull String templatePath, @Nonnull String formContainerResourceType) throws PersistenceException {
String parentPagePath = "/content/forms/af";
String parentDamAssetPath = "/content/dam/formsanddocuments";
Resource parentPageResource = resourceResolver.getResource(parentPagePath);
Resource parentDamAssetResource = resourceResolver.getResource(parentDamAssetPath);
Resource guideContainerResource = null;
if (resourceResolver != null && parentPageResource != null && parentDamAssetResource != null) {
// Create form page
Resource pageResource = createResource(parentPageResource, formName, "cq:Page");
Resource pageContentResource = createResource(pageResource, "jcr:content", "cq:PageContent", "sling:resourceType", pageResourceType, "cq:template", templatePath);
// Create guideContainer
guideContainerResource = createResource(pageContentResource, "guideContainer", "nt:unstructured", "sling:resourceType", formContainerResourceType);
// Create dam asset
Resource assetResource = createResource(parentDamAssetResource, formName, "dam:Asset");
Resource assetContentResource = createResource(assetResource, "jcr:content", "dam:AssetContent", "sling:resourceType", "fd/fm/af/render", "guide", "1");
// Create metadata
createResource(assetContentResource, "metadata", "nt:unstructured", "fd:version", "2.1", "title", formName, "formmodel", "none");
resourceResolver.commit();
} else {
logger.error("ResourceResolver is null. Cannot create AEM page.");
}
return guideContainerResource;
}
/**
* This helper method creates a resource with properties.
* It takes a parent resource, a name, a primary type, and an array of properties as parameters.
* It creates a new resource with the given properties under the parent resource.
* It returns the created resource.
*/
private Resource createResource(Resource parent, String name, String primaryType, String... properties) throws PersistenceException {
if (properties.length % 2 != 0) {
throw new IllegalArgumentException("Properties should be provided in pairs (key, value).");
}
Map<String, Object> props = new HashMap<>();
props.put("jcr:primaryType", primaryType);
for (int i = 0; i < properties.length; i += 2) {
props.put(properties[i], properties[i + 1]);
}
return resourceResolver.create(parent, name, props);
}
/**
* Returns a form as a JSON string which can be used in the content tree inside form edito
* Optionally specifying form container path, would additionally create form resources inside the specified form container path
*
* @param formContainerPath the path to the form container, or null to use the default.
* @return the generated form as a JSON string.
* @throws Exception if an error occurs during form generation.
*/
public String generateFormContainer(@Nullable String formContainerPath) throws Exception {
JsonObjectBuilder formBuilder = createObjectBuilderWithDefaults();
generateForm(formContainerPath, true, formBuilder);
JsonObject formObject = formBuilder.build();
return wrapContents(formObject);
}
private String wrapContents(JsonObject formObject) {
String contentTreeJson = "";
if (formObject != null) {
JsonObject itemsObject = formObject.getJsonObject("items");
if (itemsObject != null) {
JsonObject itemsWithoutTypeObject = removeTypeFromItems(itemsObject);
contentTreeJson = buildRootObject(itemsWithoutTypeObject);
}
}
return contentTreeJson;
}
private JsonObject removeTypeFromItems(JsonObject itemsObject) {
JsonObjectBuilder itemsWithoutTypeBuilder = Json.createObjectBuilder();
for (String key : itemsObject.keySet()) {
if (!key.equals(JcrConstants.JCR_PRIMARYTYPE)) {
itemsWithoutTypeBuilder.add(key, itemsObject.get(key));
}
}
return itemsWithoutTypeBuilder.build();
}
private String buildRootObject(JsonObject itemsWithoutTypeObject) {
JsonObjectBuilder rootBuilder = Json.createObjectBuilder();
if (itemsWithoutTypeObject.size() == 1) {
String key = itemsWithoutTypeObject.keySet().iterator().next();
rootBuilder.add("afJsonSchemaRoot", itemsWithoutTypeObject.get(key));
} else {
JsonObjectBuilder wrapperBuilder = createWrapper(itemsWithoutTypeObject);
rootBuilder.add("afJsonSchemaRoot", wrapperBuilder);
}
return rootBuilder.build().toString();
}
private JsonObjectBuilder createWrapper(JsonObject itemsWithoutTypeObject) {
JsonObjectBuilder wrapperBuilder = createObjectBuilderWithDefaults();
JsonObjectBuilder itemsWrapperBuilder = createObjectBuilderWithDefaults();
String panelResourceTypeforWrapper = resourceMappings.get("panel");
for (String key : itemsWithoutTypeObject.keySet()) {
JsonValue itemValue = itemsWithoutTypeObject.get(key);
if (itemValue.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject itemObject = (JsonObject)itemValue;
if (itemObject.containsKey("fieldType") && "panel".equals(itemObject.getString("fieldType"))) {
panelResourceTypeforWrapper = determineResourceType(itemObject);
}
}
itemsWrapperBuilder.add(key, itemValue);
}
wrapperBuilder.add("name", "wrapperPanel");
wrapperBuilder.add("fieldType", "panel");
wrapperBuilder.add("items", itemsWrapperBuilder);
wrapperBuilder.add("sling:resourceType", panelResourceTypeforWrapper);
return wrapperBuilder;
}
private void generateForm(@Nullable String formContainerPath, boolean deleteExisting, JsonObjectBuilder formBuilder) throws Exception {
logger.debug("[AF] Generating core component based form");
Resource formContainer = null;
if (StringUtils.isNotBlank(formContainerPath) && resourceResolver != null) {
formContainer = resourceResolver.getResource(formContainerPath);
}
if (deleteExisting && this.createResources && formContainer != null) {
deleteExistingChildren(formContainer);
}
// handle form container inline properties
// we don't add form container properties to the content tree, hence not passing form builder here
handleFormContainerProperties(formContainer);
// walk through the items of form container
processItems(form, formContainer, formBuilder);
if (this.createResources && formContainer != null) {
commitResourceResolver();
}
}
private void handleFormContainerProperties(@Nullable Resource formContainer) throws Exception {
Map<String, Object> formContainerPropertiesfromJson = extractCommonProperties(form);
formContainerPropertiesfromJson.put("fieldType", "form");
if (this.createResources && formContainer != null) {
// Get or create ModifiableValueMap
ModifiableValueMap valueMap = formContainer.adaptTo(ModifiableValueMap.class);
if (valueMap != null) {
// Add properties
valueMap.putAll(formContainerPropertiesfromJson);
}
}
addPropertiesToBuilder(formContainerPropertiesfromJson, null);
createCommonChildren(form, formContainer, null);
}
private void processItems(JsonObject form, @Nullable Resource parent, JsonObjectBuilder parentBuilder) throws Exception {
JsonObject itemsObject = null;
JsonArray itemsOrder = null;
JsonArray itemsArray = null;
if (form.containsKey("items")) {
itemsArray = form.getJsonArray("items");
} else if(form.containsKey(":items") && form.containsKey(":itemsOrder")) {
itemsObject = form.getJsonObject(":items");
itemsOrder = form.getJsonArray(":itemsOrder");
}
if (itemsObject != null && itemsOrder != null) {
JsonObjectBuilder itemsBuilder = createObjectBuilderWithDefaults();
// Proceed with processing items
for (JsonValue itemNameInOrder : itemsOrder) {
if (itemNameInOrder instanceof JsonString) {
String itemName = ((JsonString) itemNameInOrder).getString();
JsonValue itemValue = itemsObject.get(itemName);
if (itemValue != null) {
if (itemValue.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject item = itemValue.asJsonObject();
visitInner(item, parent, itemsBuilder);
} else {
logger.warn("Expected JsonObject but found " + itemValue.getValueType());
}
} else {
logger.warn("Item with name {} not found", itemName);
}
} else {
logger.warn("Expected JsonString but found " + itemNameInOrder.getValueType());
}
}
parentBuilder.add("items", itemsBuilder); // Add itemsBuilder to parentBuilder
} else if (itemsArray != null) {
// additional handling "items" as array too
JsonObjectBuilder itemsBuilder = createObjectBuilderWithDefaults();
// Proceed with processing items
for (JsonValue itemValue : itemsArray) {
if (itemValue.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject item = itemValue.asJsonObject();
visitInner(item, parent, itemsBuilder);
} else {
logger.warn("Expected JsonObject but found " + itemValue.getValueType());
}
}
parentBuilder.add("items", itemsBuilder); // Add itemsBuilder to parentBuilder
} else {
logger.warn("Items or itemsOrder not found in the form JsonObject");
}
}
private void deleteExistingChildren(Resource formContainer) {
try {
if (resourceResolver != null) {
for (Resource child : formContainer.getChildren()) {
resourceResolver.delete(child);
}
commitResourceResolver();
}
} catch (PersistenceException ex) {
logger.warn("Unable to delete child nodes of guideContainer", ex);
}
}
private void visitInner(JsonObject current, @Nullable Resource parent, JsonObjectBuilder parentBuilder) throws Exception {
if (current.containsKey(GuideConstants.FIELD_TYPE)) {
String fieldType = current.getString(GuideConstants.FIELD_TYPE);
JsonObjectBuilder childBuilder = createObjectBuilderWithDefaults();
Resource child = createResource(current, parent, childBuilder);
if (this.createResources && child == null) {
logger.warn("child resource is null, for parent {}", current.toString());
return;
}
if ("panel".equals(fieldType) && (current.containsKey(":items") || current.containsKey("items"))) {
processItems(current, child, childBuilder);
}
String name = generateFieldName(current, fieldType);
parentBuilder.add(name, childBuilder);
} else {
logger.info("Field type not present for {}, creating site component for the same", current.toString());
// sending fieldType as null, since we are creating site component here
JsonObjectBuilder childBuilder = createObjectBuilderWithDefaults();
createField(current, parent, null, childBuilder);
String name = generateFieldName(current, null);
parentBuilder.add(name, childBuilder);
}
}
private Resource createResource(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) {
String fieldType = field.getString(GuideConstants.FIELD_TYPE, GuideConstants.CORE_FIELD_TEXT_INPUT);
try {
switch (fieldType) {
case "panel":
return createPanel(field, parent, builder);
case GuideConstants.CORE_FIELD_TEXT_INPUT:
return createTextInput(field, parent, builder);
case GuideConstants.CORE_FIELD_EMAIL:
return createEmail(field, parent, builder);
case "file-input":
return createFileInput(field, parent, builder);
case "radio-group":
return createRadioGroup(field, parent, builder);
case "plain-text":
return createPlainText(field, parent, builder);
case "button":
case "submit":
return createButton(field, parent, builder);
case "checkbox-group":
return createCheckboxGroup(field, parent, builder);
case "checkbox":
return createCheckBox(field, parent, builder);
case "captcha":
return createCaptcha(field, parent, builder);
case "drop-down":
return createDropDown(field, parent, builder);
case "date-input":
return createDateInput(field, parent, builder);
case "number-input":
return createNumberInput(field, parent, builder);
case "sign":
return createSignBlock(field, parent, builder);
default:
logger.warn("Transformer for fieldType={} not found.", fieldType);
return createTextInput(field, parent, builder);
}
} catch (PersistenceException ex) {
logger.error("Unable to create resource", ex);
}
return null;
}
private void commitResourceResolver() {
try {
if (resourceResolver != null) {
resourceResolver.commit();
resourceResolver.refresh();
}
} catch (PersistenceException ex) {
logger.error("Unable to commit resource resolver", ex);
}
}
// Refactored create methods for various field types
private Resource createPanel(JsonObject panel, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(panel, parent, "panel", builder);
}
private Resource createTextInput(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "text-input", builder);
}
private Resource createEmail(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "email", builder);
}
private Resource createFileInput(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "file-input", builder);
}
private Resource createRadioGroup(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "radio-group", builder);
}
private Resource createPlainText(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "plain-text", builder);
}
private Resource createButton(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "button", builder);
}
private Resource createCheckboxGroup(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "checkbox-group", builder);
}
private Resource createCheckBox(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "checkbox", builder);
}
private Resource createCaptcha(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "captcha", builder);
}
private Resource createDropDown(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "drop-down", builder);
}
private Resource createDateInput(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "date-input", builder);
}
private Resource createNumberInput(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "number-input", builder);
}
private Resource createSignBlock(JsonObject field, @Nullable Resource parent, JsonObjectBuilder builder) throws PersistenceException {
return createField(field, parent, "sign", builder);
}
private Resource createField(JsonObject field, @Nullable Resource parent, String fieldType, JsonObjectBuilder builder) throws PersistenceException {
Resource resource = null;
String name = generateFieldName(field, fieldType);
if (StringUtils.isNotBlank(fieldType)) {
Map<String, Object> properties = extractCommonProperties(field);
properties.put("name", name);
properties.put("fieldType", fieldType);
addPropertiesToBuilder(properties, builder);
if (parent != null && resourceResolver != null) {
resource = resourceResolver.create(parent, name, properties);
}
createCommonChildren(field, resource, builder);
} else {
Map<String, Object> properties = extractCommonProperties(field);
addPropertiesToBuilder(properties, builder);
if (parent != null && resourceResolver != null) {
resource = resourceResolver.create(parent, name, properties);
}
}
return resource;
}
private String generateShortHashFromField(JsonObject field) {
String fieldString = field.toString();
String fullHash = DigestUtils.sha256Hex(fieldString);
return fullHash.substring(0, 10); // Return only the first 10 characters of the hash since this is node name
}
private String generateFieldName(JsonObject field, @Nullable String fieldType) {
String name = null;
if (StringUtils.isNotBlank(fieldType)) {
name = field.containsKey("name") ? field.getString("name") : (fieldType.replace("-", "_") + generateShortHashFromField(field));
} else {
name = field.containsKey("name") ? field.getString("name") : generateShortHashFromField(field);
}
return enforceName(name);
}
private String enforceName(String name) {
return name.replaceAll("[^a-zA-Z0-9]", "_");
}
private Map<String, Object> extractCommonProperties(JsonObject field) {
Map<String, Object> propertiesMap = new HashMap<>();
String resourceType = determineResourceType(field);
propertiesMap.put("sling:resourceType", resourceType);
Resource template = null;
try {
template = getTemplateNode(resourceType, resourceResolver);
} catch (Exception ex) {
logger.warn("Failed to retrieve cq:template resource for resource type: {}", resourceType);
}
if (template != null) {
ValueMap templateProperties = template.adaptTo(ValueMap.class);
if (templateProperties != null) {
for (Map.Entry<String, Object> entry : templateProperties.entrySet()) {
propertiesMap.putIfAbsent(entry.getKey(), entry.getValue());
}
} else {
logger.warn("Could not adapt template to ValueMap for resource type {}", resourceType);
}
} else {
logger.warn("Template not found for resource type {}", resourceType);
}
populateFieldOrPanelProperties(field, propertiesMap);
populateEnums(field, propertiesMap);
return propertiesMap;
}
private void populateEnums(JsonObject field, Map<String, Object> propertiesMap) {
if (field.containsKey("enum")) {
JsonArray enumArray = field.getJsonArray("enum");
List<Object> enumList = new ArrayList<>();
for (JsonValue value : enumArray) {
switch(value.getValueType()) {
case STRING:
enumList.add(((JsonString)value).getString());
break;
case NUMBER:
JsonNumber num = (JsonNumber)value;
if (num.isIntegral()) {
enumList.add(num.longValue());
} else {
enumList.add(num.doubleValue());
}
break;
default:
throw new IllegalArgumentException("Unsupported enum type");
}
}
propertiesMap.put("enum", enumList.toArray());
propertiesMap.put("enumNames", enumList.toArray());
}
if (field.containsKey("enumNames")) {
String[] enumNamesArr = field.getJsonArray("enumNames").getValuesAs(JsonString.class).stream()
.map(JsonString::getString).toArray(String[]::new);
propertiesMap.put("enumNames", (String[]) enumNamesArr);
}
}
private String determineResourceType(JsonObject field) {
String resourceType = field.containsKey(":type") ? field.getString(":type") : resourceMappings.get(field.getString("fieldType"));
return resourceType != null ? resourceType : resourceMappings.get("text-input");
}
private void populateFieldOrPanelProperties(JsonObject field, Map<String, Object> propertiesMap) {
List<String> layoutIgnoreList = Arrays.asList("columnCount", "columnClassNames", "gridClassNames", "allowedComponents");
// events are handles separately hence adding it in the keysToIgnore list
// enum and enumNames are handled separately, hence adding it to the ignore list for safetly
List<String> keysToIgnore = Arrays.asList(":items", "items", ":type", ":itemsOrder", "dataLayer", "id", "events", "rules", "metadata", "allowedComponents", "action", "enum", "enumNames");
for (Map.Entry<String, JsonValue> entry : field.entrySet()) {
String key = entry.getKey();
JsonValue value = entry.getValue();
if (!keysToIgnore.contains(key) && !layoutIgnoreList.contains(key)) {
switch (key) {
case "label":
handleLabel(value.asJsonObject(), propertiesMap);
break;
case "constraintMessages":
if (value.getValueType() == JsonValue.ValueType.OBJECT) {
handleConstraintMessages(value, propertiesMap);
}
break;
case "properties":
handleProperties(value, propertiesMap);
break;
default:
handleDefault(key, value, propertiesMap);
break;
}
}
}
}
private void handleProperties(JsonValue value, Map<String, Object> propertiesMap) {
List<String> keysToIgnore = Arrays.asList("fd:path", "fd:formDataEnabled", "fd:schemaType");
if (value.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject propertiesObject = value.asJsonObject();
for (Map.Entry<String, JsonValue> propEntry : propertiesObject.entrySet()) {
String propKey = propEntry.getKey();
JsonValue propValue = propEntry.getValue();
if (!keysToIgnore.contains(propKey)) {
switch (propKey) {
case "afs:layout":
case "fd:dor":
if (propValue.getValueType() == JsonValue.ValueType.OBJECT) {
handleComplexProperty(propValue.asJsonObject(), propertiesMap);
}
break;
default:
handleDefault(propKey, propValue, propertiesMap);
break;
}
}
}
}
}
private void handleConstraintMessages(JsonValue constraintMessagesValue, Map<String, Object> propertiesMap) {
if (constraintMessagesValue.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject constraintMessages = constraintMessagesValue.asJsonObject();
Map<String, String> constraintMessageMap = new HashMap<>();
constraintMessageMap.put("required", "mandatoryMessage");
constraintMessageMap.put("minimum", "minimumMessage");
constraintMessageMap.put("maximum", "maximumMessage");
constraintMessageMap.put("minLength", "minLengthMessage");
constraintMessageMap.put("maxLength", "maxLengthMessage");
constraintMessageMap.put("step", "stepMessage");
constraintMessageMap.put("format", "formatMessage");
constraintMessageMap.put("minItems", "minItemsMessage");
constraintMessageMap.put("maxItems", "maxItemsMessage");
constraintMessageMap.put("uniqueItems", "uniqueItemsMessage");
constraintMessageMap.put("enforceEnum", "enforceEnumMessage");
constraintMessageMap.put("validationExpression", "validateExpMessage");
constraintMessageMap.put("maxFileSize", "maxFileSizeMessage");
constraintMessageMap.put("accept", "acceptMessage");
for (Map.Entry<String, JsonValue> entry : constraintMessages.entrySet()) {
String key = entry.getKey();
JsonValue value = entry.getValue();
String mappedKey = constraintMessageMap.getOrDefault(key, key);
handleJsonValue(mappedKey, value, propertiesMap);
}
}
}
private void handleComplexProperty(JsonObject complexObject, Map<String, Object> propertiesMap) {
for (Map.Entry<String, JsonValue> entry : complexObject.entrySet()) {
handleJsonValue(entry.getKey(), entry.getValue(), propertiesMap);
}
}
private void handleLabel(JsonValue labelValue, Map<String, Object> propertiesMap) {
if (labelValue.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject labelObject = labelValue.asJsonObject();
handleJsonValue("jcr:title", labelObject.get("value"), propertiesMap);
JsonValue labelVisible = labelObject.get("visible");
if (labelVisible != null) {
handleJsonValue("hideTitle", labelVisible, propertiesMap);
}
}
}
// General handler for default properties
private void handleDefault(String key, JsonValue value, Map<String, Object> propertiesMap) {
Map<String, String> jsonToJcrMap = new HashMap<>();
jsonToJcrMap.put("adaptiveform", "specVersion");
jsonToJcrMap.put("richText", "textIsRich");
jsonToJcrMap.put("screenReaderText", "assistPriority");
String mappedKey = jsonToJcrMap.getOrDefault(key, key);
// Check if the mappedKey requires special handling
if ("assistPriority".equals(mappedKey)) {
// Special handling for assistPriority
propertiesMap.put("assistPriority", "custom");
handleJsonValue("custom", value, propertiesMap);
}
handleJsonValue(mappedKey, value, propertiesMap);
}
// Helper method to handle JsonValue extraction and conversion
private void handleJsonValue(String key, JsonValue value, Map<String, Object> propertiesMap) {
if (value instanceof JsonString) {
propertiesMap.put(key, ((JsonString) value).getString());
} else if (value instanceof JsonNumber) {
JsonNumber number = (JsonNumber) value;
if (number.isIntegral()) {
propertiesMap.put(key, number.longValue());
} else {
propertiesMap.put(key, number.doubleValue());
}
} else if (value.getValueType() == JsonValue.ValueType.TRUE || value.getValueType() == JsonValue.ValueType.FALSE) {
propertiesMap.put(key, value.getValueType() == JsonValue.ValueType.TRUE);
} else {
propertiesMap.put(key, value.toString());
}
}
/**
* Create common child nodes
*/
private Resource createCommonChildren(JsonObject element, @Nullable Resource field, @Nullable JsonObjectBuilder parentBuilder) throws PersistenceException {
if (field != null) {
String resourceType = field.getResourceType();
Resource template = null;
try {
template = getTemplateNode(resourceType, resourceResolver);
} catch (Exception ex) {
logger.warn("Failed to retrieve cq:template resource for resource type: {}", resourceType, ex);
}
if (template != null) {
// should we copy all children?
Resource rules = template.getChild(GuideConstants.FD_RULES);
if (rules != null) {
resourceResolver.copy(rules.getPath(), field.getPath());
}
Resource events = template.getChild(GuideConstants.FD_EVENTS);
if (events != null) {
resourceResolver.copy(events.getPath(), field.getPath());
}
resourceResolver.commit();
}
}
// events
boolean eventsExist = element.containsKey("events");
if(eventsExist) {
JsonObject events = element.getJsonObject("events");
JsonObjectBuilder eventsBuilder = createObjectBuilderWithDefaults();
Map<String, Object> properties = new HashMap<>();
events.forEach((key, value) -> {
switch (value.getValueType()) {
case ARRAY:
JsonArrayBuilder eventsArrayBuilder = Json.createArrayBuilder();
value.asJsonArray().forEach(arrayValue -> {
if (arrayValue.getValueType() == JsonValue.ValueType.STRING) {
eventsArrayBuilder.add(((JsonString) arrayValue).getString());
} else {
eventsArrayBuilder.add(arrayValue.toString());
}
});
eventsBuilder.add(key, eventsArrayBuilder);
String[] eventsValue = value.asJsonArray().getValuesAs(JsonString.class).stream()
.map(JsonString::getString).toArray(String[]::new);
properties.put(key, eventsValue);
break;
case STRING:
JsonString eventValue = (JsonString) value;
properties.put(key, new String[]{eventValue.getString()});
JsonArrayBuilder eventsArrayBuilder1 = Json.createArrayBuilder();
eventsArrayBuilder1.add(((JsonString) eventValue).getString());
eventsBuilder.add(key, eventsArrayBuilder1);
}
});
if (field != null) {
this.resourceResolver.create(field, GuideConstants.FD_EVENTS, properties);
}
if (parentBuilder != null) {
parentBuilder.add(GuideConstants.FD_EVENTS, eventsBuilder);
}
}
// rules
boolean rulesExist = element.containsKey("rules") || element.containsKey(GuideConstants.FD_RULES);
if(rulesExist) {
Map<String, Object> properties = new HashMap<>();
JsonObjectBuilder rulesBuilder = createObjectBuilderWithDefaults();
if(element.containsKey("rules")) {
JsonObject rules = element.getJsonObject("rules");
rules.forEach((key, value) -> {
if (value.getValueType() == JsonValue.ValueType.STRING) {
JsonString eventValue = (JsonString) value;
properties.put(key, eventValue.getString());
rulesBuilder.add(key, eventValue.getString());
} else {
logger.warn("rules should be of type string, got type=" + value.getValueType().name());
}
});
}
if(element.containsKey(GuideConstants.FD_RULES)) {
JsonObject rules = element.getJsonObject(GuideConstants.FD_RULES);
rules.forEach((key, value) -> {
if (value.getValueType() == JsonValue.ValueType.ARRAY) {
JsonArrayBuilder rulesArrayBuilder = Json.createArrayBuilder();
JsonArray array = value.asJsonArray();
array.forEach(arrayValue -> {
if (arrayValue.getValueType() == JsonValue.ValueType.STRING) {
rulesArrayBuilder.add(((JsonString) arrayValue).getString());
} else {
rulesArrayBuilder.add(arrayValue.toString());
}
});
rulesBuilder.add(key, rulesArrayBuilder);
String[] valueArr = array.getValuesAs(JsonString.class).stream()
.map(JsonString::getString).toArray(String[]::new);
properties.put(key, valueArr);
} else {
logger.warn("rules should be of type string, got type=" + value.getValueType().name());
}
});
}
if (field != null) {
this.resourceResolver.create(field, GuideConstants.FD_RULES, properties);
}
if (parentBuilder != null) {
parentBuilder.add(GuideConstants.FD_RULES, rulesBuilder.build());
}
}
return field;
}
/**
* Returns the template Node of the field from resource type.
*
* @param resourceType
* @param resolver
* @return
* @throws Exception
* @pad.exclude exclude from published api
*/
private Resource getTemplateNode(String resourceType, ResourceResolver resolver) throws Exception {
Resource templateNode = null;
if (resolver != null && StringUtils.isNotBlank(resourceType)) {
ComponentManager componentManager = resolver.adaptTo(ComponentManager.class);
// get the resource at the given type
Component component = componentManager.getComponent(resourceType);
// walk through the super component hierarchy
while (component != null) {
// get the template json of the resource
templateNode = resolver.getResource(component.getTemplatePath());
if (templateNode != null) {
break;
}
component = component.getSuperComponent();
}
}
return templateNode;
}
// Helper method to handle java objects into javax.json types
// only java arrays are handled and not list
private void addPropertiesToBuilder(Map<String, Object> properties, @Nullable JsonObjectBuilder builder) {
if (builder != null) {
properties.forEach((key, value) -> {
if (value instanceof String) {
builder.add(key, (String) value);
} else if (value instanceof Number) {
Number numberValue = (Number) value;
if (numberValue instanceof Long) {
builder.add(key, (Long) numberValue);
} else if (numberValue instanceof Double) {
builder.add(key, (Double) numberValue);
} else {
// Handle other Number subtypes if needed
builder.add(key, numberValue.toString());
}
} else if (value instanceof Boolean) {
builder.add(key, (Boolean) value);
} else if (value instanceof JsonObject) {
builder.add(key, (JsonObject) value);
} else if (value instanceof JsonArray) {
builder.add(key, (JsonArray) value);
} else if (value instanceof String[]) { // handling only arrays, since properties map can only have array type
List<String> stringList = Arrays.asList((String[]) value);
JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
stringList.forEach(arrayBuilder::add);
builder.add(key, arrayBuilder.build());
} else if (value instanceof Object[]) { // handling only arrays, since properties map can only have array type
List<Object> objectList = Arrays.asList((Object[]) value);
JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
objectList.forEach(item -> {
if (item instanceof String) {
arrayBuilder.add((String) item);
} else if (item instanceof Number) {
Number numberItem = (Number) item;
if (numberItem instanceof Long) {
arrayBuilder.add((Long) numberItem);
} else if (numberItem instanceof Double) {
arrayBuilder.add((Double) numberItem);
} else {
arrayBuilder.add(numberItem.toString());
}
} else if (item instanceof Boolean) {
arrayBuilder.add((Boolean) item);
} else {
arrayBuilder.add(item.toString());
}
});
builder.add(key, arrayBuilder.build());
} else {
// Handle other types as needed
// For example, convert them to strings
builder.add(key, value.toString());
}
});
}
}
}
@rismehta
Copy link
Author

rismehta commented Jun 19, 2024

You can use the above API in the following way,

 CoreComponentFormGenerator coreComponentFormGenerator = new CoreComponentFormGenerator(resourceResolver, <headless_form_json_as_string>, null);
 
 // send form name, template path, page resource type, af container resource type as parameter
 // this would create form inside "/content/forms/af" with the form name mentioned
 
  coreComponentFormGenerator.generateForm("temp", "/conf/core-components-examples/settings/wcm/templates/af-blank-v2", "forms-components-examples/components/page", "forms-components-examples/components/form/container");

@rismehta
Copy link
Author

One needs to install the forms core component package from here, https://github.com/adobe/aem-core-forms-components/releases for the above code to work. This artifact contains all the forms core components.

@rismehta
Copy link
Author

Since customers can have their own proxy component, you can define same in headless form JSON via the :type property pointing to the proxy resource type. You can check a sample here, https://github.com/adobe/aem-core-forms-components/blob/master/bundles/af-core/src/test/resources/form/checkbox/exporter-checkbox.json#L30

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment