From 8ce425184674ecb9d7f3e74742b3be82dc8da15f Mon Sep 17 00:00:00 2001 From: Shivam Agarwal Date: Thu, 14 May 2026 01:01:06 +0530 Subject: [PATCH] feat: auto-convert fragments referenced by a form during job execution After converting a form, scan its JCR tree for fragRef/fragmentPath references. For each found fragment: copy to /fragments/, convert in place using the same rule pipeline, then update all path references in the converted form. A convertedFragments set skips duplicates within the same bucket. Warnings (path||message) are stored in the bucket resource for any node that still carries an AF1 sling:resourceType after conversion. --- .../form/job/FormConversionJobExecutor.java | 246 +++++++++++++++++- 1 file changed, 238 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/adobe/aem/modernize/form/job/FormConversionJobExecutor.java b/core/src/main/java/com/adobe/aem/modernize/form/job/FormConversionJobExecutor.java index c8123e2b..b2f391c4 100644 --- a/core/src/main/java/com/adobe/aem/modernize/form/job/FormConversionJobExecutor.java +++ b/core/src/main/java/com/adobe/aem/modernize/form/job/FormConversionJobExecutor.java @@ -31,6 +31,9 @@ import com.day.cq.wcm.api.PageManager; import com.day.cq.wcm.api.WCMException; import org.apache.commons.lang3.StringUtils; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.flat.TreeTraverser; +import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; @@ -43,6 +46,10 @@ import javax.jcr.Node; import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -60,6 +67,11 @@ public class FormConversionJobExecutor extends AbstractConversionJobExecutor { private static final String AF_ROOT = "/content/forms/af"; private static final String DAM_ROOT = "/content/dam/formsanddocuments"; + private static final String FRAGMENTS_SUBFOLDER = "fragments"; + private static final String FRAG_REF_PROPERTY = "fragRef"; + private static final String FRAGMENT_PATH_PROPERTY = "fragmentPath"; + private static final String SLING_RESOURCE_TYPE = "sling:resourceType"; + private static final String AF1_RESOURCE_TYPE_PREFIX = "fd/af/"; @Reference private ComponentRewriteRuleService componentService; @@ -71,16 +83,21 @@ public class FormConversionJobExecutor extends AbstractConversionJobExecutor { protected void doProcess(@NotNull Job job, @NotNull JobExecutionContext context, @NotNull ConversionJobBucket bucket) { final ConversionJob.PageHandling pageHandling = getPageHandling(bucket); String sourceRoot = getSourceRoot(bucket); - String targetRoot = getTargetRoot(bucket); Resource resource = bucket.getResource(); ResourceResolver rr = resource.getResourceResolver(); PageManager pm = rr.adaptTo(PageManager.class); + String targetRoot = normalizeTargetRoot(rr, getTargetRoot(bucket)); Set componentRules = getComponentRules(bucket); List paths = bucket.getPaths(); context.initProgress(paths.size(), -1); + // Shared across all forms in this bucket — each unique fragment converted at most once + Set convertedFragments = new HashSet<>(); + // path → list of human-readable warnings written to the JCR bucket node for UI display + List bucketWarnings = new ArrayList<>(); + for (String path : paths) { Page page = pm.getPage(path); if (page == null) { @@ -94,42 +111,77 @@ protected void doProcess(@NotNull Job job, @NotNull JobExecutionContext context, continue; } + long startMs = System.currentTimeMillis(); + context.log("Converting form [{0}] ({1}/{2})", path, paths.indexOf(path) + 1, paths.size()); + logger.info("Starting conversion for form [{}]", path); try { if (pageHandling == RESTORE) { + context.log("Restoring previous version for [{0}]", path); page = RewriteUtils.restore(pm, page); } + context.log("Creating pre-conversion version for [{0}]", path); RewriteUtils.createVersion(pm, page); if (pageHandling == COPY) { - // copy asset part for forms + context.log("Copying form asset and page for [{0}]", path); Node formAssetNode = rr.getResource(getFormsAssetPathFromPagePath(page.getPath())).adaptTo(Node.class); Node targetNode = rr.getResource(getFormsAssetPathFromPagePath(targetRoot)).adaptTo(Node.class); JcrUtil.copy(formAssetNode, targetNode, formAssetNode.getName()); - - // copy page part for forms page = RewriteUtils.copyPage(pm, page, sourceRoot, targetRoot); } + convertReferencedFragments(page, pageHandling, targetRoot, componentRules, pm, rr, context, convertedFragments, path, bucketWarnings); + if (componentRules.isEmpty()) { - context.log("No component rules found, skipping skipping component conversion."); + context.log("No component rules found, skipping component conversion for [{0}].", path); } else { + context.log("Applying {0} component rule(s) to form [{1}]", componentRules.size(), page.getPath()); + long ruleMs = System.currentTimeMillis(); componentService.apply(page.getContentResource(), componentRules, true); + context.log("Form rules applied in {0}ms", System.currentTimeMillis() - ruleMs); + collectWarnings(page.getContentResource(), path, null, bucketWarnings, context); + } + + if (pageHandling == COPY && !convertedFragments.isEmpty()) { + try { + updateFragmentPaths(page.adaptTo(Node.class).getNode(JcrConstants.JCR_CONTENT), + targetRoot + "/" + FRAGMENTS_SUBFOLDER); + rr.adaptTo(Session.class).save(); + } catch (RepositoryException e) { + logger.warn("Could not update fragment paths in converted form [{}]: {}", page.getPath(), e.getMessage()); + } } + long elapsed = System.currentTimeMillis() - startMs; + context.log("Successfully converted [{0}] in {1}ms", path, elapsed); + logger.info("Completed conversion for form [{}] in {}ms", path, elapsed); bucket.getSuccess().add(path); } catch (WCMException e) { - logger.error("Error occurred while trying to manage page versions.", e); + bucketWarnings.add(path + "||Conversion failed: " + e.getMessage()); + logger.error("Error occurred while trying to manage page versions for [{}]", path, e); + context.log("Failed to manage page version for [{0}]: {1}", path, e.getMessage()); bucket.getFailed().add(path); } catch (RewriteException e) { - logger.error("Conversion resulted in an error", e); + logger.error("Conversion resulted in an error for [{}]", path, e); + context.log("Conversion failed for [{0}]: {1}", path, e.getMessage()); + bucketWarnings.add(path + "||Conversion error: " + e.getMessage()); bucket.getFailed().add(path); } catch (RepositoryException e) { - logger.error("Failed to copy forms asset node. Conversion resulted in an error", e); + logger.error("Failed to copy forms asset node for [{}]", path, e); + context.log("Repository error during conversion of [{0}]: {1}", path, e.getMessage()); + bucketWarnings.add(path + "||Repository error: " + e.getMessage()); bucket.getFailed().add(path); } context.incrementProgressCount(1); } + + if (!bucketWarnings.isEmpty()) { + ModifiableValueMap mvm = bucket.getResource().adaptTo(ModifiableValueMap.class); + if (mvm != null) { + mvm.put("warnings", bucketWarnings.toArray(new String[0])); + } + } } @Override @@ -140,4 +192,182 @@ protected ResourceResolverFactory getResourceResolverFactory() { private String getFormsAssetPathFromPagePath(String pagePath) { return StringUtils.replace(pagePath, AF_ROOT, DAM_ROOT); } + + // If targetRoot was provided as a DAM path (/content/dam/formsanddocuments/...), + // translate it to the corresponding forms/af path and create the folder if absent. + private String normalizeTargetRoot(ResourceResolver rr, String targetRoot) { + if (StringUtils.isBlank(targetRoot)) { + return targetRoot; + } + String afPath = targetRoot.startsWith(DAM_ROOT) + ? StringUtils.replace(targetRoot, DAM_ROOT, AF_ROOT) + : targetRoot; + if (rr.getResource(afPath) == null) { + try { + JcrUtil.createPath(afPath, JcrConstants.NT_UNSTRUCTURED, "sling:Folder", rr.adaptTo(Session.class), false); + } catch (javax.jcr.RepositoryException e) { + logger.warn("Could not create target folder at [{}]: {}", afPath, e.getMessage()); + } + } + return afPath; + } + + private void convertReferencedFragments(Page formPage, ConversionJob.PageHandling pageHandling, + String targetRoot, Set componentRules, + PageManager pm, ResourceResolver rr, JobExecutionContext context, + Set convertedFragments, String formPath, List bucketWarnings) { + + Set fragRefs; + try { + Node contentNode = formPage.adaptTo(Node.class).getNode(JcrConstants.JCR_CONTENT); + fragRefs = collectFragmentRefs(contentNode); + } catch (RepositoryException e) { + logger.warn("Could not scan fragment refs in [{}]: {}", formPage.getPath(), e.getMessage()); + return; + } + + if (fragRefs.isEmpty()) { + return; + } + + context.log("Found {0} fragment reference(s) in [{1}]", fragRefs.size(), formPage.getPath()); + int fragIdx = 0; + for (String fragRef : fragRefs) { + fragIdx++; + if (convertedFragments.contains(fragRef)) { + context.log(" [{0}/{1}] Fragment already converted, skipping: {2}", fragIdx, fragRefs.size(), fragRef); + continue; + } + + // fragRef may be a DAM path (/content/dam/formsanddocuments/...) — resolve to the AF page path + String fragPagePath = fragRef.startsWith(DAM_ROOT) + ? StringUtils.replace(fragRef, DAM_ROOT, AF_ROOT) + : fragRef; + Page fragPage = pm.getPage(fragPagePath); + if (fragPage == null) { + context.log(" [{0}/{1}] Fragment page not found, skipping: {2}", fragIdx, fragRefs.size(), fragRef); + continue; + } + + context.log(" [{0}/{1}] Converting fragment: {2}", fragIdx, fragRefs.size(), fragPage.getName()); + long fragMs = System.currentTimeMillis(); + try { + Resource convertedFragResource; + if (pageHandling == COPY) { + convertedFragResource = convertFragmentCopy(fragPage, targetRoot, componentRules, pm, rr, context); + } else { + convertFragmentInPlace(fragPage, componentRules, context); + convertedFragResource = fragPage.getContentResource(); + } + convertedFragments.add(fragRef); + long fragElapsed = System.currentTimeMillis() - fragMs; + context.log(" [{0}/{1}] Fragment [{2}] done in {3}ms", fragIdx, fragRefs.size(), + fragPage.getName(), fragElapsed); + collectWarnings(convertedFragResource, formPath, "fragment " + fragPage.getName(), bucketWarnings, context); + } catch (WCMException | RewriteException | RepositoryException e) { + context.log(" [{0}/{1}] Fragment [{2}] FAILED: {3}", fragIdx, fragRefs.size(), + fragPage.getName(), e.getMessage()); + bucketWarnings.add(formPath + "||Fragment " + fragPage.getName() + " failed: " + e.getMessage()); + logger.error("Fragment conversion failed for [{}]", fragRef, e); + } + } + } + + private Resource convertFragmentCopy(Page fragPage, String targetRoot, Set componentRules, + PageManager pm, ResourceResolver rr, JobExecutionContext context) + throws WCMException, RewriteException, RepositoryException { + + String fragmentsTarget = targetRoot + "/" + FRAGMENTS_SUBFOLDER; + + ensureFolderExists(rr, fragmentsTarget); + ensureFolderExists(rr, getFormsAssetPathFromPagePath(fragmentsTarget)); + + String fragDamPath = getFormsAssetPathFromPagePath(fragPage.getPath()); + Resource fragDamResource = rr.getResource(fragDamPath); + if (fragDamResource != null) { + Node fragDamNode = fragDamResource.adaptTo(Node.class); + Node damTargetFolder = rr.getResource(getFormsAssetPathFromPagePath(fragmentsTarget)).adaptTo(Node.class); + JcrUtil.copy(fragDamNode, damTargetFolder, fragDamNode.getName()); + } + + String fragParent = fragPage.getPath().substring(0, fragPage.getPath().lastIndexOf('/')); + Page copiedFrag = RewriteUtils.copyPage(pm, fragPage, fragParent, fragmentsTarget); + + if (!componentRules.isEmpty()) { + componentService.apply(copiedFrag.getContentResource(), componentRules, true); + } + return copiedFrag.getContentResource(); + } + + private void convertFragmentInPlace(Page fragPage, Set componentRules, + JobExecutionContext context) throws RewriteException { + + if (componentRules.isEmpty()) { + return; + } + + componentService.apply(fragPage.getContentResource(), componentRules, true); + } + + private Set collectFragmentRefs(Node contentNode) throws RepositoryException { + Set refs = new LinkedHashSet<>(); + for (Node node : new TreeTraverser(contentNode)) { + if (node.hasProperty(FRAG_REF_PROPERTY)) { + refs.add(node.getProperty(FRAG_REF_PROPERTY).getString()); + } + } + return refs; + } + + private void updateFragmentPaths(Node contentNode, String fragmentsTargetFolder) + throws RepositoryException { + for (Node node : new TreeTraverser(contentNode)) { + if (node.hasProperty(FRAGMENT_PATH_PROPERTY)) { + String oldPath = node.getProperty(FRAGMENT_PATH_PROPERTY).getString(); + String fragName = oldPath.substring(oldPath.lastIndexOf('/') + 1); + node.setProperty(FRAGMENT_PATH_PROPERTY, fragmentsTargetFolder + "/" + fragName); + } + } + } + + private void ensureFolderExists(ResourceResolver rr, String path) { + if (rr.getResource(path) == null) { + try { + JcrUtil.createPath(path, JcrConstants.NT_UNSTRUCTURED, "sling:Folder", + rr.adaptTo(Session.class), false); + } catch (RepositoryException e) { + logger.warn("Could not create folder at [{}]: {}", path, e.getMessage()); + } + } + } + + private void collectWarnings(Resource resource, String formPath, String label, + List bucketWarnings, JobExecutionContext context) { + if (resource == null) { + return; + } + try { + Node contentNode = resource.adaptTo(Node.class); + if (contentNode == null) { + return; + } + int count = 0; + for (Node node : new TreeTraverser(contentNode)) { + if (node.hasProperty(SLING_RESOURCE_TYPE)) { + String rt = node.getProperty(SLING_RESOURCE_TYPE).getString(); + if (rt.startsWith(AF1_RESOURCE_TYPE_PREFIX)) { + String prefix = label != null ? label + ": " : ""; + bucketWarnings.add(formPath + "||" + prefix + node.getPath() + " — unconverted resource type: " + rt); + count++; + } + } + } + if (count > 0) { + context.log(" {0} component(s) not converted in [{1}]", count, + label != null ? label : resource.getPath()); + } + } catch (RepositoryException e) { + logger.warn("Could not scan for unconverted components in [{}]", resource.getPath(), e); + } + } }