From 565b9213fd1d3a63f2402dcdff8eb5e0312b6c99 Mon Sep 17 00:00:00 2001 From: Harwinder Singh Date: Fri, 8 Sep 2023 16:03:59 -0400 Subject: [PATCH 01/10] Feature/mcptools/bulk page tag creator (#3171) * MCP Tool : AEM Bulk Pages Tagger --------- Co-authored-by: david g --- CHANGELOG.md | 3 + .../mcp/impl/processes/BulkPageTagger.java | 254 ++++++++++++++++++ .../impl/processes/BulkPageTaggerFactory.java | 39 +++ .../impl/processes/BulkPageTaggerTest.java | 90 +++++++ .../mcp/impl/processes/bulkPageTagger.xlsx | Bin 0 -> 9907 bytes 5 files changed, 386 insertions(+) create mode 100644 bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTagger.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerFactory.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerTest.java create mode 100644 bundle/src/test/resources/com/adobe/acs/commons/mcp/impl/processes/bulkPageTagger.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 785f222695..c1e1d247f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) ## Unreleased ([details][unreleased changes details]) +#3170 - Added a new MCP tool to bulk tag AEM content pages via an Excel file input. + + ## Fixed - #3040 - Fixed bug where namespaced multi-fields would have the namespace 2 times diff --git a/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTagger.java b/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTagger.java new file mode 100644 index 0000000000..e92f23dc46 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTagger.java @@ -0,0 +1,254 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2023 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.mcp.impl.processes; + +import com.adobe.acs.commons.fam.ActionManager; +import com.adobe.acs.commons.mcp.ProcessDefinition; +import com.adobe.acs.commons.mcp.ProcessInstance; +import com.adobe.acs.commons.mcp.form.FileUploadComponent; +import com.adobe.acs.commons.mcp.form.FormField; +import com.adobe.acs.commons.mcp.model.GenericBlobReport; +import com.adobe.acs.commons.mcp.util.StringUtil; +import com.day.cq.tagging.Tag; +import com.day.cq.wcm.api.Page; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Iterator; +import java.util.HashSet; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The type Bulk page tagger. + */ +public class BulkPageTagger extends ProcessDefinition implements Serializable { + private static final Logger log = LoggerFactory.getLogger(BulkPageTagger.class); + private static final long serialVersionUID = 798823856839772874L; + + /** + * The constant NAME. + */ + public static final String NAME = "Bulk Page Tagger"; + + /** + * The Excel file. + */ + @FormField( + name = "Excel File", + description = "Provide the .xlsx file that defines the content pages and the corresponding cq:tags to be added on the pages", + component = FileUploadComponent.class, + options = {"mimeTypes=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "required"} + ) + public transient InputStream excelFile = null; + + + @Override + public void init() throws RepositoryException { + // Nothing to be done here. + + } + + + @Override + public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws LoginException { + report.setName(instance.getName()); + instance.getInfo().setDescription("Bulk Tag AEM content Pages"); + instance.defineCriticalAction("Parse Excel File", rr, this::parseExcel); + instance.defineCriticalAction("Add Tags to Content Pages", rr, this::tagPages); + + } + + + /** + * The Page tag mapping dataStructure. + */ + transient volatile HashMap pageTagMapping = new LinkedHashMap<>(); + + /** + * Parse input excel. + * + * @param manager the manager + * @throws Exception the exception + */ + @SuppressWarnings("squid:S112") + public void parseExcel(ActionManager manager) throws Exception { + manager.withResolver(rr -> { + final XSSFWorkbook workbook = new XSSFWorkbook(excelFile); + final XSSFSheet sheet = workbook.getSheetAt(0); + + final Iterator rows = sheet.rowIterator(); + final String tagsRootPath = new TagCreator.TagRootResolver(rr).getTagsLocationPath(); + + + if (tagsRootPath == null) { + recordAction(ReportRowSatus.FAILED_TO_PARSE, + "Abandoning Tag parsing. Unable to determine AEM Tags root (/content/cq:tags vs /etc/tags). Please ensure the path exists and is accessible by the user running Tag Creator.", "N/A"); + return; + } + + while (rows.hasNext()) { + final Row row = rows.next(); + if (row.getCell(0) == null) { + break; + } + if (row.getRowNum() != 0 && row.getCell(0) != null) { + pageTagMapping.put(row.getCell(0).getStringCellValue(), row.getCell(1).getStringCellValue()); + } + } + + }); + + } + + + /** + * Tag pages from the excel file with cq:tags. + * + * @param manager the manager + * @throws Exception the exception + */ + @SuppressWarnings("squid:S112") + public void tagPages(ActionManager manager) throws Exception { + + manager.withResolver(rr -> { + + pageTagMapping.forEach((key, value) -> { + BulkPageTagger.ReportRowSatus status; + Resource resource = rr.getResource(key); + if (resource != null) { + Page page = resource.adaptTo(Page.class); + if (page != null) { + Tag[] existingPageTags = page.getTags(); + String[] tagIds = Stream.of(existingPageTags) + .map(Tag::getTagID) + .toArray(String[]::new); + Set updatedTags = Arrays.stream(value.split("[;\n]")) + .map(String::trim) + .collect(Collectors.toSet()); + updatedTags.addAll(Arrays.asList(tagIds)); + String[] updatedTagsArray = updatedTags.stream().toArray(String[]::new); + + ModifiableValueMap properties = page.getContentResource().adaptTo(ModifiableValueMap.class); + properties.put(com.day.cq.tagging.TagConstants.PN_TAGS, updatedTagsArray); + try { + rr.commit(); + status = ReportRowSatus.UPDATED_EXISTING; + recordAction(status, page.getPath(), Arrays.toString(updatedTagsArray)); + } catch (PersistenceException e) { + status = ReportRowSatus.FAILED_TO_UPDATE; + recordAction(status, page.getPath(), Arrays.toString(updatedTagsArray)); + log.error(String.format("Unable to add tags to page with page path - %s ", page.getPath())); + } + + + } + + } + + }); + }); + } + + + /** + * Reporting + **/ + + + private final transient GenericBlobReport report = new GenericBlobReport(); + + private final transient ArrayList> reportRows = new ArrayList<>(); + + private enum ReportColumns { + /** + * Status report columns. + */ + STATUS, + /** + * Page path report columns. + */ + PAGE_PATH, + /** + * Tags array report columns. + */ + TAGS_ARRAY + } + + /** + * The enum Report row satus. + */ + public enum ReportRowSatus { + /** + * Created report row satus. + */ + CREATED, + /** + * Updated existing report row satus. + */ + UPDATED_EXISTING, + /** + * Failed to parse report row satus. + */ + FAILED_TO_PARSE, + /** + * Failed to update report row satus. + */ + FAILED_TO_UPDATE + } + + + private void recordAction(BulkPageTagger.ReportRowSatus status, String pagePath, String tags) { + final EnumMap row = new EnumMap<>(BulkPageTagger.ReportColumns.class); + + row.put(BulkPageTagger.ReportColumns.STATUS, StringUtil.getFriendlyName(status.name())); + row.put(ReportColumns.PAGE_PATH, pagePath); + row.put(ReportColumns.TAGS_ARRAY, tags); + + reportRows.add(row); + } + + + @Override + public void storeReport(ProcessInstance instance, ResourceResolver rr) throws RepositoryException, PersistenceException { + report.setRows(reportRows, BulkPageTagger.ReportColumns.class); + report.persist(rr, instance.getPath() + "/jcr:content/report"); + } + + +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerFactory.java b/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerFactory.java new file mode 100644 index 0000000000..0c9ba7c79d --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerFactory.java @@ -0,0 +1,39 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2023 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.mcp.impl.processes; + +import com.adobe.acs.commons.mcp.ProcessDefinitionFactory; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; + +@Component +@Service(ProcessDefinitionFactory.class) +public class BulkPageTaggerFactory extends ProcessDefinitionFactory { + + @Override + public String getName() { + return BulkPageTagger.NAME; + } + + @Override + protected BulkPageTagger createProcessDefinitionInstance() { + return new BulkPageTagger(); + } +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerTest.java b/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerTest.java new file mode 100644 index 0000000000..3e08035a41 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/BulkPageTaggerTest.java @@ -0,0 +1,90 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2023 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.mcp.impl.processes; + +import com.adobe.acs.commons.fam.ActionManager; +import com.adobe.acs.commons.functions.CheckedConsumer; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class BulkPageTaggerTest { + + @Rule + public final AemContext context = new AemContext(ResourceResolverType.JCR_MOCK); + + private BulkPageTagger bulkPageTagger; + + @Mock + private ActionManager actionManager; + + @Before + public void setUp() throws Exception { + bulkPageTagger = new BulkPageTagger(); + bulkPageTagger.excelFile = getClass().getResourceAsStream("/com/adobe/acs/commons/mcp/impl/processes/bulkPageTagger.xlsx"); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + CheckedConsumer method = (CheckedConsumer) invocation.getArguments()[0]; + method.accept(context.resourceResolver()); + return null; + } + }).when(actionManager).withResolver(any(CheckedConsumer.class)); + + } + + @Test + public void testParseExcel() throws Exception { + + final String tagsRootPath = com.day.cq.tagging.TagConstants.TAG_ROOT_PATH; + context.create().resource(tagsRootPath, JcrConstants.JCR_PRIMARYTYPE, "sling:Folder"); + context.resourceResolver().commit(); + bulkPageTagger.parseExcel(actionManager); + + final int expected = 9; + assertEquals(expected,bulkPageTagger.pageTagMapping.size() ); + } + + @Test + public void testTagPages() throws Exception { + + bulkPageTagger.parseExcel(actionManager); + context.create().page("/content/wknd/language-masters/en/about-us"); + context.create().resource("/content/wknd/language-masters/en/about-us/jcr:content", + JcrConstants.JCR_PRIMARYTYPE, + "cq:pagecontent","cq:tags","wknd-shared:activity/bulktagtest3;wknd-shared:activity/bulktagtest10"); + bulkPageTagger.tagPages(actionManager); + + } +} diff --git a/bundle/src/test/resources/com/adobe/acs/commons/mcp/impl/processes/bulkPageTagger.xlsx b/bundle/src/test/resources/com/adobe/acs/commons/mcp/impl/processes/bulkPageTagger.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e62257641dc74fc425f90af0f7ad7287d279b812 GIT binary patch literal 9907 zcmeHN2UL^Uwgw_igdj+lrYKFCKoFz|*pMM0MUWOzkrDz(??n*=L>#&lkrs?VP(tWc z1Vkht0+CQc5rl*u2?8NJ(0512JL8?Z)_ZT=yWZVdD>?tkKHq=-Z|}4JeTtDjV9x;> zCMG5tCQku#n(u^nr|jn4}4jbeRP7*3`hq|lBfpQhzjMsYuZ@{H%^E~a9`b^U%N3@j9^4w z)G(sZwuRnOjG>DHrASo~B?r&zY^gJ!pz^PdyfSWh-4A&+a-f}7(6HrLwQj(xsOLv* z8xrl$`Ywc5Isyd4J6~0u_&hoCT&e?vmXLONZaZ4?26dFeeJ^>nni_wtnc^7EC-kuF!8_T*FJ;xoSNO7Z?h&Wko?k@=5Dw0j=XYj5A< zi>_db&DAzeA(0KXA{;Ki;fQ!-!Qs6yy~?5b*^gkVu{r5x?)HchaMLr;e~Y~Sjyy6u zn;2UuR|>Id*qU8`w@oe8+P2dO_6YQ)lKa;)h?P9u+0@Ai@~kj~GK5;gs%YUG8mg)S zJ+#K==ShX7iwhy^)VVqG6fw4lG_;5et@aDtR?)J>TW8@%wwI?1Q9}?{SqvmN=xu)n zDnzY+uz!7Os4)9zP$)@85gAHZUYTmmMnW2DYy1M&oL+UcW{_$(D?>LoR#JyT!L=Ln zZ>x$rQ&q5)Sgq0EwUrG5DSy*xkq>P7rkXu`v zSWI=3U8F8A2W#A0UtNFatK!zUtv(3^x=pq~8ydGM`0aqz>1>|ufr+8fwY;=Vm+gg- z?SYkT6+{-sa`UQbrk3mSyc>}+)skQ2CO}$}TUpot(29ybyd6NA7+kHbRY`S|qXv*Z zkVpE1DWMei<>`$rQsEk;DqCx*vNPKjAN;v#FxJ5VSi1Q+W4pS}FEkdfwZ3FeStAm& zwJ76qr6FsFY&So~Z;!2Ilcs8iYKcYLb)P>;;@*ju%7&H|*kki%-&SB<{q{_$9Jm`3 zvbEIPZBAL5jkzz4YvFQOBMC*4qjI+1_E1}UsE#wAvAXRoJ*ImMDDte8LlET>Wkh+a3O=Mf zS#{&R@=a?_9|Iw5zKuz{vOV_AWV#c!crx7{J2#mwgZ&g0nkBr^fmBuVB@V}9M%32w zJS#CNDc<|iEXPCcI@b>}}GtYf2 z$NZIox0fu@`rOAd%wH*xX4Fea>S~5W^?1xXweCDpIp$|ob@wryYNdIkG7NF1u9&2z zCP(~f)#glH2}x7UjaV0t8B$x!BUNC2Ry7PhHv?>}EIO5(XgRf~IMY;G+C=%)_`k^HgeLg_|S3d}}f7$vpF!)I*4wOJdwMu^(~+ zf4$$V_dFW zH#rA7RfZ4~kQnSsU`=y*+Q9$@tzHL6WWg)8e~+CI3pmBGtFZ?2qU~@cmU#Z0_bI=mve=lD4K8Xh0DUioJ<~qddd+70`54| zLRNJc;T^+c5SKIHVg^5&7GaQ#YwCl8VPVcDh>8y3IB4J%G~$v#6|S#{^$JXCfT0Ye z37>wX(uy7C9W2i|jXbg?875I$nD}^n$MN}}_W$;(fJ~!YRYI+a94-0cK z=>Qp3AnXO?hfGndnJ~g2Ll1}x25`zqub@R6m<@s4Sk{9@M|pcs>bk?su|8E5MbMMh zL;Yw$YwZKW3Dh&8DjlThfXeFtf-nuX079BG6 zNWv|~IdGJFbu+h4f#7@AAKJGVw=(?%FE}-Su~F-_Kz>M6Qsw!->i}>xVSxPGz5j;% zkGklt9aiyY{r^*p#7>2BjlaghHw;_reR=c0C+ORsf5s!Zqu@MF=pQou0}#ZT3I5OD zs>|N5fi0e8+3Sdq^~pXo04Yeo(PG)W=fj{;H9k#fU7M3*xE!RQ`@9jBt;**Kl<_yU zw^jR$hgyE7@kc;eu|{Z(-!T42!+m}U-DpiA7KK(*u-I)6+9mT#G@i`QDn1^%0-=9n z-_*dC#u6TUL_U>n)AEWe1G&G+nyvT`ySU#vtYn9iKkok@-`fa#|HF#!jtBoG4*m?| z2de&6)L5%PeP~*2WDRHxXM|<279illpt?2!?yx6N0Fu=k)`ZSM{_81?cUP}yn}_x$ z$JcD2kU{VJvwfx}kr4mUWMG|hpymppyKW`NJd}_eU%%m58no1(9W<3!`tR{<{{wt; zA`QG@FVG02K^3SU&5VWDfW~pA*rV1N1Y88v#3sWX_7wW>t~48{cGE;0-yc1sw%!09 ze&6BiGcf>esN_`f-FUqK9v;;j^1LmMACItrOoQ4Q z;uTbmle6wsCoh|mk5Aud!#x_}+YD(N?s@O4G@a7+uf&qi3$%&N$#EP4ndLrjiUn8s zJcWu|pX^6-BC`lMW-QoyJ_1@;^B=W;f{>6)Kp=7XU4Wl;d^WZP5J-6`Bs4hbKwjz* z(n3eIlRsB$s8@ZI7GGPM5rTEM1X4Z;3E}hp4-7z$mGz##1x>B-X-4bY)Q;f_kT&k~ z##q@Z9|V-gy0#C^g0vyv{-gH4NJxmR3=qDW=;W_dl5No0;%F!nnK3YiuC^LA3PuI)z)AV&x|R;;V{d?d88#-{~sWm7wj zD?yI9&tJm2R{0b`|D*Qzgb3G?=g6zAts~i(MGQne(A$$rU70sDbZux1TBXc(L2S|S z*!sqX_4P?JIVx~>Y1RGcU<(%)4bAnWpDnE({<=#2-O?(=!`|il1y$q1ZZcfWM6iUo4hsmJ+DOl;+Yn7XsLD%`7Uwo04fM?m?f1cs6?;{s* z0M90w(I4vGtO*7t1otge}bo6$HiS%LUEv=|E1@2Hrqekbdrs!}bumNyR8d z#ij?HMU8y)z=&8S2dE>&f}NfFgWc;1Wy-4Owm)t7gS0zo)Ax$UIrlF)lV&+euj?GA zOFGFZT^AXh)Vf!6zl0(;kH2=>2KSVDi?M)x3P~S z;Bz5i6)-&B73u|u_Sl%R_lwnO!yqURGjZqmsEMR{x|xZm^BrM9Qs*N8cWS34;YT!V z_p7@CU#1DJ-bE$Vzmt+tI1cEya~OHjNIZO+Sd#IWRqd3D zPqNPC%MK*+1JI`cZWfcUQay;=3yWgu7lQj0d?Dg5Dli3CRdcV}X%*7(nO$V0A1i}? zwk=?~OJl;3;{B9j*sl>-P?#{tI>g89Hk(z_E4aVShZrMYVRuWnS0%f8pmWL(k#&)e z)Ed~w&$`I)x<+y72_};$ole68PITZ_uhZl2hX{qQp-x;?1)l_~9 z5u>GlWIPQ15J$YhCjmtdVum-WCQ=64I}Z|g9-iLM{2`(s>!}-8#|&2GR;ONwX)aLj zGt)VuS}YEY_QpsswrOj|+qWG_6R zolkgAn+GboPBpPV=$gvRqUH5E>DA&j!=a9{b;jL+W}g>sC$Y1KQ^NMMfp*~QK=X98 z^RT~e;^ncsFSffK)}N&9Iwa42#<1rk)39QBmdJS%y~}%MYGOap9QHW0pCd}v!4azw z+zt+-frZobOX;(1haS?>>eHnBqVL)CLczqL!d&2;$oj!UaarrsPyM40Bk3iY5|~eV z>VSY-g(;W$(Hy=vI0o@2+e2RL2})i&Uqi~&r+aJ9C%DL>Gsb+ecfu#5t3;o@LrK#- z0WQ5SiY2*IG5SN71iX!u1(Nrm&KU#@@*Nc${GA@OFzl=kEn?KP;jk98+% z^#OUnA=t<&H_EJuLk`XCm*WgMW|~5$Z{0qnYT1F#jMfYGF}e-8^U1%1UZ>*7=HSK= zxb|$(gLcWXV-}z8;w)rPf)f5V|HWFZrUq`A z^6XpmTfmgX%en{54ECI^Hd^f!D*M$#x$W2`O8DI1ffb@+ThB=UC4!W(72BA>KI*N^6%wj8&#_dODf8!(4RFgL+E(WtWg#C@L{Hca=Vl?Tlg?76iM zlvK{hyTHqmn;IDvCy?Lz%g4I$upInzdB2V!c5%KgBx+7k(fV>a??M|TGFDM2FT%Ik z``LbsJl6ZdD^IwUPiK|I!#Ap3Zgac~5ONDwFWW)Wyz*O4K4*$@J-O-jySnA%V_A&2 z#^dTd;x?1U&pv<34nAo}K}tH=IFLB)I^%x11GK#B5JrM|ANo+4Z~wSeBhVn!cxB_RPgK z3)yJl+0me1eCK49jJSdf65f~j6^EQg_TZf{p{YMP4s?1T{wovnGE?{bRm(9c-nR=hTW$`U4i-yHr@rHc-Wf9f5;uimknKU{TlgMQiEPSD@k+-8s58O^6h zFOB8ra-9})d3wP`$`s8gPmZwod}JtpoGhrr5nUE-v95{_D%)O#_e(i7F*+G&m-%1d zkRRUb#74iqPEC{1<2a;vOA`86#DVF$iDpS?iPl1i#!(A59c==y4cKW+*^zsCAiO#8 zbwJl4*{yJfkKxfqfYT*2`soc9yYfZNbruTcUCi{@7E`j7(){lD86C?Fep0{!FsSrB zAyZx1eCZ~pWI@ldNwWN)TFzA5gK3j{N~HG04AI`4R$D2)?kwzGr@bC4M;G}}7blGq zuWi@8b*>t|5$iPS8(*DUB(Mnz48E6Cpwvmt;hv+)z@ZA737?eSd_Ej}`n7`VNyAR> zcHowup2N#4pyRLV=@NM1H>cUb5ReSqmeRszg<}!Hy-Qf4WJ||J4#-)3@*K@g;#PNzis~`5I#H4r0p^ zjW0xJ8c19pf-|!BzCZr_h^PT>gPa8#Q)AcRv5nqtrEK>&qPF-0mcu1|o28$`aO?@- zboppSzZjLeDbD_|ys`!;#C0#t&I%8+eU5#pIOwgw!nS-l{BTUS@r_CMo@%E38h9m9 z)^rQ_rL(a6HD{`&I?p|v<}-p^kEu6%K(3&>$$OY8ep1Q6!JX%g(c^N#e7y=MPj3Jo z77Lqd!dqX(3Y&+1@!b@@W)s2=AJud}i)<`A8ei%9?;KZ~?Up<{ea}vgdxLE3Zg(%| znZi*QR-wlgx}v8For+$}&l|_Fq$aMDLg2-z{#SvY>_ns=zw)@^$Ti!{Pm3kjY0$7- zw3HDqlep4KsFQMoW!s$ z+r2j2C}0$vR{WMOD39gJJYh*VqsG<9?~r%)+cse?NVJ%~oBUpjqf{}i(YtNOa+-<~ zLe(wmzc@#}TaHNC&Vxw&!xYy_*Ui<--qj0Y>hEsvX|p?2(I%bX9j~C!QAJm{lwDOt zBGSoClT7E6fEIHG*ZSrBN3eL5o~77Rk?=m-x{CIbibQlFDGSxg!1-`YT0ycs_f+XE zP{`%z_*T*k_&`!K^jY(@Bjoc}HEYvl$@>vw*A0^$J?^g=Cph=eCX41eNPqrIyLn!G-#x|J@myk!VS4l+(hun%|hf}Vn?qcb}SH3iByQ(08T zi%jvj|03mkDde_Tt-K^K`4OH``GHa6$Cq(%Q8_KP%y&`XXZ9G;F2L#bjw8hGz}S{M zzOPi16+>ALCJ@3x>KuzyQ`qip&vyk!K2sxfE;w{Id|@4Sx7xqn3Y!-?OI zfZr#2yYr@R@|5Qb;E%JY9|68kI(GBHZ^9|?1>o#h=%45hPRPEJ;N^g-kneSPACM3?Oy#CKJE*@ literal 0 HcmV?d00001 From e9ad2765376d1e2a929b26450fe8bf91838693fe Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Fri, 8 Sep 2023 22:11:14 +0200 Subject: [PATCH 02/10] #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet (#3167) * #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet --- CHANGELOG.md | 1 + .../processes/cfi/ContentFragmentImport.java | 24 +++++++------------ .../cfi/ContentFragmentImportTest.java | 2 -- .../processes/cfi/MockContentFragment.java | 14 ++++++++++- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e1d247f9..0050eaa251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) - ## Unreleased ([details][unreleased changes details]) +- #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet #3170 - Added a new MCP tool to bulk tag AEM content pages via an Excel file input. diff --git a/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImport.java b/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImport.java index 2510cf8a04..efe48f9ff5 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImport.java +++ b/bundle/src/main/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImport.java @@ -33,7 +33,6 @@ import com.adobe.cq.dam.cfm.FragmentTemplate; import com.day.cq.commons.jcr.JcrConstants; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -48,22 +47,17 @@ import javax.jcr.RepositoryException; import javax.jcr.Session; import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang3.reflect.MethodUtils; import org.apache.sling.api.request.RequestParameter; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Import a series of content fragments from a spreadsheet */ public class ContentFragmentImport extends ProcessDefinition { - private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentImport.class); - public enum ReportColumns { ITEM, ACTION, DESCRIPTION, COUNT } @@ -313,8 +307,14 @@ private void setContentElements(ContentFragment cf, Map row, ContentElement conte protected ContentFragment getOrCreateFragment(Resource parent, Resource template, String name, String title) throws ContentFragmentException { Resource fragmentResource = parent.getChild(name); if (fragmentResource == null) { - try { - FragmentTemplate fragmentTemplate = template.adaptTo(FragmentTemplate.class); -// TODO: Replace this reflection hack with the proper method once ACS Commons doesn't support 6.2 anymore - return (ContentFragment) MethodUtils.invokeMethod(fragmentTemplate, "createFragment", parent, name, title); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { - LOG.error("Unable to call createFragment method -- Is this 6.3 or newer?", ex); - return null; - } + FragmentTemplate fragmentTemplate = template.adaptTo(FragmentTemplate.class); + return fragmentTemplate.createFragment(parent, name, title); } else { return fragmentResource.adaptTo(ContentFragment.class); } diff --git a/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImportTest.java b/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImportTest.java index 07a1e4c13b..038ded4657 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImportTest.java +++ b/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/ContentFragmentImportTest.java @@ -28,8 +28,6 @@ import java.util.Date; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; diff --git a/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/MockContentFragment.java b/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/MockContentFragment.java index 8c7698bf3e..82cc8676e9 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/MockContentFragment.java +++ b/bundle/src/test/java/com/adobe/acs/commons/mcp/impl/processes/cfi/MockContentFragment.java @@ -37,6 +37,10 @@ import org.apache.sling.api.resource.Resource; import org.jetbrains.annotations.NotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + /** * Incomplete mock that provides just enough for basic testing */ @@ -47,6 +51,14 @@ public class MockContentFragment implements ContentFragment { String path; HashMap elements = new HashMap<>(); HashMap metadata = new HashMap<>(); + FragmentTemplate template; + + public MockContentFragment(){ + template = mock(FragmentTemplate.class); + ElementTemplate elementTemplate = mock(ElementTemplate.class); + doReturn("text/html").when(elementTemplate).getInitialContentType(); + doReturn(elementTemplate).when(template).getForElement(any(ContentElement.class)); + } @Override public Iterator getElements() { @@ -114,7 +126,7 @@ public Iterator listAllVariations() { @Override public FragmentTemplate getTemplate() { - return null; + return template; } @Override From ad10e452281ef23021437f25d786d5c3193c29da Mon Sep 17 00:00:00 2001 From: Roy Teeuwen Date: Fri, 8 Sep 2023 22:18:49 +0200 Subject: [PATCH 03/10] Add page property injection (#3160) * Add page property injection --------- Co-authored-by: Roy Teeuwen --- CHANGELOG.md | 5 +- .../annotation/HierarchicalPageProperty.java | 14 +++-- .../injectors/annotation/PageProperty.java | 61 +++++++++++++++++++ .../HierarchicalPagePropertyInjector.java | 21 +++++-- .../HierarchicalPagePropertyInjectorTest.java | 20 +++--- .../TestHierarchicalPagePropertiesModel.java | 4 +- .../impl/model/TestPagePropertiesModel.java | 27 ++++++++ ...rarchicalPagePropertiesModelModelImpl.java | 12 +++- .../TestPagePropertiesModelModelImpl.java | 49 +++++++++++++++ .../injectors/impl/we-retail-pages.json | 3 + 10 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/PageProperty.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestPagePropertiesModel.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestPagePropertiesModelModelImpl.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0050eaa251..408b3fe47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) ## Unreleased ([details][unreleased changes details]) - #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet -#3170 - Added a new MCP tool to bulk tag AEM content pages via an Excel file input. +## Added + +- #3159 - Add PageProperty annotation for Sling Models +- #3170 - Added a new MCP tool to bulk tag AEM content pages via an Excel file input. ## Fixed diff --git a/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/HierarchicalPageProperty.java b/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/HierarchicalPageProperty.java index a0b5b24808..a99caf95ba 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/HierarchicalPageProperty.java +++ b/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/HierarchicalPageProperty.java @@ -18,6 +18,7 @@ package com.adobe.acs.commons.models.injectors.annotation; +import com.adobe.acs.commons.models.injectors.impl.HierarchicalPagePropertyInjector; import org.apache.commons.lang3.StringUtils; import org.apache.sling.models.annotations.Source; import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; @@ -39,14 +40,9 @@ @Target({METHOD, FIELD, PARAMETER}) @Retention(RUNTIME) @InjectAnnotation -@Source(HierarchicalPageProperty.SOURCE) +@Source(HierarchicalPagePropertyInjector.SOURCE) public @interface HierarchicalPageProperty { - /** - * Source value used for this annotation. - * @see Source - */ - String SOURCE = "hierarchical-page-property"; /** * Specifies the name of the value from the value map to take. @@ -54,6 +50,12 @@ */ String value() default StringUtils.EMPTY; + /** + * Specifies if it should use the hierarchy to search for the page property. + * If false, it will only look at the current page. + */ + boolean inherit() default true; + /** * if set to REQUIRED injection is mandatory, if set to OPTIONAL injection is optional, in case of DEFAULT * the standard annotations ({@link org.apache.sling.models.annotations.Optional}, {@link org.apache.sling.models.annotations.Required}) are used. diff --git a/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/PageProperty.java b/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/PageProperty.java new file mode 100644 index 0000000000..4e2f73a20c --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/models/injectors/annotation/PageProperty.java @@ -0,0 +1,61 @@ +/* + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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.adobe.acs.commons.models.injectors.annotation; + + +import com.adobe.acs.commons.models.injectors.impl.HierarchicalPagePropertyInjector; +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.models.annotations.Source; +import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; +import org.apache.sling.models.spi.injectorspecific.InjectAnnotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Injects a page property. + * Note: not supported by the javax.Inject annotation because of performance reasons. Only direct annotation is supported. + */ +@Target({METHOD, FIELD, PARAMETER}) +@Retention(RUNTIME) +@InjectAnnotation +@Source(HierarchicalPagePropertyInjector.SOURCE) +public @interface PageProperty { + + + /** + * Specifies the name of the value from the value map to take. + * If empty, then the name is derived from the method or field. + */ + String value() default StringUtils.EMPTY; + + + /** + * if set to REQUIRED injection is mandatory, if set to OPTIONAL injection is optional, in case of DEFAULT + * the standard annotations ({@link org.apache.sling.models.annotations.Optional}, {@link org.apache.sling.models.annotations.Required}) are used. + * If even those are not available the default injection strategy defined on the {@link org.apache.sling.models.annotations.Model} applies. + * Default value = DEFAULT. + */ + InjectionStrategy injectionStrategy() default InjectionStrategy.DEFAULT; + +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjector.java b/bundle/src/main/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjector.java index 37ef6c49ff..706784875c 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjector.java +++ b/bundle/src/main/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjector.java @@ -18,11 +18,13 @@ package com.adobe.acs.commons.models.injectors.impl; import com.adobe.acs.commons.models.injectors.annotation.HierarchicalPageProperty; +import com.adobe.acs.commons.models.injectors.annotation.PageProperty; import com.adobe.acs.commons.util.impl.ReflectionUtil; import com.day.cq.commons.inherit.HierarchyNodeInheritanceValueMap; import com.day.cq.commons.inherit.InheritanceValueMap; import com.day.cq.wcm.api.Page; import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.Source; import org.apache.sling.models.spi.DisposalCallbackRegistry; import org.apache.sling.models.spi.Injector; import org.osgi.framework.Constants; @@ -42,15 +44,22 @@ ) public class HierarchicalPagePropertyInjector implements Injector { + /** + * Source value used for injector + * + * @see Source + */ + public static final String SOURCE = "hierarchical-page-property"; + @Override public String getName() { - return HierarchicalPageProperty.SOURCE; + return SOURCE; } public Object getValue(Object adaptable, String name, Type declaredType, AnnotatedElement element, DisposalCallbackRegistry callbackRegistry) { - if (!element.isAnnotationPresent(HierarchicalPageProperty.class)) { + if (!(element.isAnnotationPresent(HierarchicalPageProperty.class) || element.isAnnotationPresent(PageProperty.class))) { //skipping javax.Inject for performance reasons. Only supports direct injection. return null; } @@ -59,8 +68,12 @@ public Object getValue(Object adaptable, String name, Type declaredType, Annotat if (currentResource != null) { Resource adaptableRes = lookUpFromPage(currentResource); if (adaptableRes != null) { - InheritanceValueMap inheritanceValueMap = new HierarchyNodeInheritanceValueMap(adaptableRes); - return ReflectionUtil.convertValueMapValue(inheritanceValueMap, name, declaredType); + if (element.isAnnotationPresent(PageProperty.class) || !element.getAnnotation(HierarchicalPageProperty.class).inherit()) { + return ReflectionUtil.convertValueMapValue(adaptableRes.getValueMap(), name, declaredType); + } else { + InheritanceValueMap inheritanceValueMap = new HierarchyNodeInheritanceValueMap(adaptableRes); + return ReflectionUtil.convertValueMapValue(inheritanceValueMap, name, declaredType); + } } } diff --git a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjectorTest.java b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjectorTest.java index fc0e57ecc9..2ad5625cc3 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjectorTest.java +++ b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/HierarchicalPagePropertyInjectorTest.java @@ -17,10 +17,11 @@ */ package com.adobe.acs.commons.models.injectors.impl; -import com.adobe.acs.commons.models.injectors.annotation.HierarchicalPageProperty; import com.adobe.acs.commons.models.injectors.annotation.impl.HierarchicalPagePropertyAnnotationProcessorFactory; import com.adobe.acs.commons.models.injectors.impl.model.TestHierarchicalPagePropertiesModel; +import com.adobe.acs.commons.models.injectors.impl.model.TestPagePropertiesModel; import com.adobe.acs.commons.models.injectors.impl.model.impl.TestHierarchicalPagePropertiesModelModelImpl; +import com.adobe.acs.commons.models.injectors.impl.model.impl.TestPagePropertiesModelModelImpl; import io.wcm.testing.mock.aem.junit.AemContext; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.spi.Injector; @@ -32,8 +33,7 @@ import org.mockito.InjectMocks; import org.mockito.junit.MockitoJUnitRunner; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; @RunWith(MockitoJUnitRunner.class) public class HierarchicalPagePropertyInjectorTest { @@ -43,7 +43,8 @@ public class HierarchicalPagePropertyInjectorTest { @InjectMocks private HierarchicalPagePropertyInjector injector; - private TestHierarchicalPagePropertiesModel adapted; + private TestHierarchicalPagePropertiesModel hierarchicalModel; + private TestPagePropertiesModel pageModel; @Before public void setUp() throws Exception { @@ -53,19 +54,24 @@ public void setUp() throws Exception { context.registerService(Injector.class, injector); context.registerService(StaticInjectAnnotationProcessorFactory.class, new HierarchicalPagePropertyAnnotationProcessorFactory()); context.addModelsForClasses(TestHierarchicalPagePropertiesModelModelImpl.class); + context.addModelsForClasses(TestPagePropertiesModelModelImpl.class); Resource adaptable = context.request().getResource(); - adapted = adaptable.adaptTo(TestHierarchicalPagePropertiesModel.class); + hierarchicalModel = adaptable.adaptTo(TestHierarchicalPagePropertiesModel.class); + pageModel = adaptable.adaptTo(TestPagePropertiesModel.class); } @Test public void test_name() { - assertEquals(HierarchicalPageProperty.SOURCE, injector.getName()); + assertEquals(HierarchicalPagePropertyInjector.SOURCE, injector.getName()); } @Test public void test() { - assertNotNull(adapted); + assertNotNull(hierarchicalModel); + assertEquals("inherited!", hierarchicalModel.getHierarchicalPagePropertyString()); + assertNull(pageModel.getHierarchicalPagePropertyString()); + assertEquals("not inherited", hierarchicalModel.getPagePropertyString()); } diff --git a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestHierarchicalPagePropertiesModel.java b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestHierarchicalPagePropertiesModel.java index 4365350c2b..b38acaebb2 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestHierarchicalPagePropertiesModel.java +++ b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestHierarchicalPagePropertiesModel.java @@ -23,7 +23,9 @@ public interface TestHierarchicalPagePropertiesModel { - String getPropertyString(); + String getPagePropertyString(); + + String getHierarchicalPagePropertyString(); String getUndefinedProperty(); diff --git a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestPagePropertiesModel.java b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestPagePropertiesModel.java new file mode 100644 index 0000000000..757d52cfc2 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/TestPagePropertiesModel.java @@ -0,0 +1,27 @@ +/* + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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.adobe.acs.commons.models.injectors.impl.model; + + +public interface TestPagePropertiesModel { + + String getPagePropertyString(); + + String getHierarchicalPagePropertyString(); + +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestHierarchicalPagePropertiesModelModelImpl.java b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestHierarchicalPagePropertiesModelModelImpl.java index b786963cdb..ae255f5284 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestHierarchicalPagePropertiesModelModelImpl.java +++ b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestHierarchicalPagePropertiesModelModelImpl.java @@ -34,10 +34,13 @@ ) public class TestHierarchicalPagePropertiesModelModelImpl implements TestHierarchicalPagePropertiesModel { + @HierarchicalPageProperty + private String pagePropertyString; + @HierarchicalPageProperty private String hierarchicalPagePropertyString; - @HierarchicalPageProperty("hierarchicalPagePropertyBoolean") + @HierarchicalPageProperty(value = "hierarchicalPagePropertyBoolean", inherit = false) private boolean hierarchicalPagePropertyBoolean; @HierarchicalPageProperty @@ -63,7 +66,12 @@ public class TestHierarchicalPagePropertiesModelModelImpl implements TestHierarc @Override - public String getPropertyString() { + public String getPagePropertyString() { + return pagePropertyString; + } + + @Override + public String getHierarchicalPagePropertyString() { return hierarchicalPagePropertyString; } diff --git a/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestPagePropertiesModelModelImpl.java b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestPagePropertiesModelModelImpl.java new file mode 100644 index 0000000000..7faa5d76c4 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/models/injectors/impl/model/impl/TestPagePropertiesModelModelImpl.java @@ -0,0 +1,49 @@ +/* + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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.adobe.acs.commons.models.injectors.impl.model.impl; + +import com.adobe.acs.commons.models.injectors.annotation.PageProperty; +import com.adobe.acs.commons.models.injectors.impl.model.TestPagePropertiesModel; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.DefaultInjectionStrategy; +import org.apache.sling.models.annotations.Model; + +@Model( + adapters = TestPagePropertiesModel.class, + adaptables = {SlingHttpServletRequest.class, Resource.class}, + defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL +) +public class TestPagePropertiesModelModelImpl implements TestPagePropertiesModel { + + @PageProperty + private String hierarchicalPagePropertyString; + @PageProperty + private String pagePropertyString; + + @Override + public String getPagePropertyString() { + return pagePropertyString; + } + + @Override + public String getHierarchicalPagePropertyString() { + return hierarchicalPagePropertyString; + } + +} diff --git a/bundle/src/test/resources/com/adobe/acs/commons/models/injectors/impl/we-retail-pages.json b/bundle/src/test/resources/com/adobe/acs/commons/models/injectors/impl/we-retail-pages.json index 6a80e34a7d..6baabc8f61 100644 --- a/bundle/src/test/resources/com/adobe/acs/commons/models/injectors/impl/we-retail-pages.json +++ b/bundle/src/test/resources/com/adobe/acs/commons/models/injectors/impl/we-retail-pages.json @@ -7,6 +7,7 @@ "cq:template": "/conf/we-retail/settings/wcm/templates/hero-page", "cq:designPath": "/etc/designs/acs-commons", "hierarchicalPagePropertyString": "inherited!", + "pagePropertyString": "inherited!", "hierarchicalPagePropertyBoolean": true, "hierarchicalPagePropertyInteger": 11, "hierarchicalPagePropertyMultiValueInteger": [ @@ -168,6 +169,8 @@ "cq:lastModified": "Tue Nov 06 2018 11:00:39 GMT+0100", "sling:resourceType": "weretail/components/structure/page", "cq:lastModifiedBy": "admin", + "pagePropertyString": "not inherited", + "hierarchicalPagePropertyBoolean": false, "root": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wcm/foundation/components/responsivegrid", From 2f4ececbfef5b5c4659e1ea97bd6ddca55b78c9f Mon Sep 17 00:00:00 2001 From: david g Date: Fri, 8 Sep 2023 16:22:18 -0400 Subject: [PATCH 04/10] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 408b3fe47a..571a10cd0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) - ## Unreleased ([details][unreleased changes details]) -- #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet +## 6.1.0 - 2023-09-08 ## Added @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) ## Fixed +- #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet - #3040 - Fixed bug where namespaced multi-fields would have the namespace 2 times - #3140 - Fixed issue where malformed MCP process nodes can cause a NPE that breaks the entire MPC reporting UI. Now displays more friendly values in UI to help remove the invalid nodes. - #3150 - Support for case-insensitive redirect rules ( [NC] flag equivalent of apache) From 79ab882d3b164367570929709ea4e68d6d6ef5e2 Mon Sep 17 00:00:00 2001 From: david g Date: Fri, 8 Sep 2023 16:24:37 -0400 Subject: [PATCH 05/10] [maven-release-plugin] prepare release acs-aem-commons-6.1.0 --- all/pom.xml | 2 +- bundle/pom.xml | 2 +- oakpal-checks/pom.xml | 2 +- pom.xml | 6 +++--- ui.apps/pom.xml | 2 +- ui.content/pom.xml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 316f7f5b33..060bea5f2f 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.0.15-SNAPSHOT + 6.1.0 diff --git a/bundle/pom.xml b/bundle/pom.xml index 43f83c3c3e..f3e1413b18 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.0.15-SNAPSHOT + 6.1.0 diff --git a/oakpal-checks/pom.xml b/oakpal-checks/pom.xml index 0259a6bdfe..16ca4b0aab 100644 --- a/oakpal-checks/pom.xml +++ b/oakpal-checks/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.0.15-SNAPSHOT + 6.1.0 diff --git a/pom.xml b/pom.xml index df76f9f47d..39135a38e1 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.0.15-SNAPSHOT + 6.1.0 pom ACS AEM Commons - Reactor Project @@ -49,7 +49,7 @@ scm:git:git@github.com:Adobe-Consulting-Services/acs-aem-commons.git scm:git:git@github.com:Adobe-Consulting-Services/acs-aem-commons.git https://github.com/Adobe-Consulting-Services/acs-aem-commons/tree/master - acs-aem-commons-5.5.0 + acs-aem-commons-6.1.0 @@ -88,7 +88,7 @@ 1.8 1.8 - 1689084806 + 1694204582 diff --git a/ui.apps/pom.xml b/ui.apps/pom.xml index 09a79d924d..4cea8f843f 100644 --- a/ui.apps/pom.xml +++ b/ui.apps/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.0.15-SNAPSHOT + 6.1.0 diff --git a/ui.content/pom.xml b/ui.content/pom.xml index 861c9c86b1..1e819044f7 100644 --- a/ui.content/pom.xml +++ b/ui.content/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.0.15-SNAPSHOT + 6.1.0 From a75bb2920aeefb274e3d89b32b65b6293b704822 Mon Sep 17 00:00:00 2001 From: david g Date: Fri, 8 Sep 2023 16:24:39 -0400 Subject: [PATCH 06/10] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- bundle/pom.xml | 2 +- oakpal-checks/pom.xml | 2 +- pom.xml | 6 +++--- ui.apps/pom.xml | 2 +- ui.content/pom.xml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 060bea5f2f..098119bbd4 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.1.0 + 6.1.1-SNAPSHOT diff --git a/bundle/pom.xml b/bundle/pom.xml index f3e1413b18..c0d2b8c9ec 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.1.0 + 6.1.1-SNAPSHOT diff --git a/oakpal-checks/pom.xml b/oakpal-checks/pom.xml index 16ca4b0aab..75c32575f5 100644 --- a/oakpal-checks/pom.xml +++ b/oakpal-checks/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.1.0 + 6.1.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 39135a38e1..167e1cbffd 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.1.0 + 6.1.1-SNAPSHOT pom ACS AEM Commons - Reactor Project @@ -49,7 +49,7 @@ scm:git:git@github.com:Adobe-Consulting-Services/acs-aem-commons.git scm:git:git@github.com:Adobe-Consulting-Services/acs-aem-commons.git https://github.com/Adobe-Consulting-Services/acs-aem-commons/tree/master - acs-aem-commons-6.1.0 + acs-aem-commons-5.5.0 @@ -88,7 +88,7 @@ 1.8 1.8 - 1694204582 + 1694204679 diff --git a/ui.apps/pom.xml b/ui.apps/pom.xml index 4cea8f843f..5a036d06cb 100644 --- a/ui.apps/pom.xml +++ b/ui.apps/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.1.0 + 6.1.1-SNAPSHOT diff --git a/ui.content/pom.xml b/ui.content/pom.xml index 1e819044f7..45ff9b0550 100644 --- a/ui.content/pom.xml +++ b/ui.content/pom.xml @@ -25,7 +25,7 @@ com.adobe.acs acs-aem-commons - 6.1.0 + 6.1.1-SNAPSHOT From d9fb0cd6258c392eb032fd932f1fa34b3f8632db Mon Sep 17 00:00:00 2001 From: Mark Adamcin Date: Thu, 14 Sep 2023 07:27:45 -0700 Subject: [PATCH 07/10] resolves #3183 removes JackrabbitSessionIWrap and related classes (#3184) --- CHANGELOG.md | 4 + .../wrap/impl/SessionLogoutGuardFactory.java | 96 ----- .../jackrabbit/JackrabbitSessionIWrap.java | 75 ---- .../commons/wrap/jackrabbit/package-info.java | 22 -- .../commons/wrap/jcr/BaseSessionIWrap.java | 329 ------------------ .../acs/commons/wrap/jcr/SessionIWrap.java | 30 -- .../acs/commons/wrap/jcr/package-info.java | 22 -- .../impl/SessionLogoutGuardFactoryTest.java | 70 ---- .../JackrabbitSessionIWrapTest.java | 114 ------ .../wrap/jcr/BaseSessionIWrapTest.java | 215 ------------ .../commons/wrap/jcr/SessionIWrapTest.java | 53 --- 11 files changed, 4 insertions(+), 1026 deletions(-) delete mode 100644 bundle/src/main/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactory.java delete mode 100644 bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrap.java delete mode 100644 bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/package-info.java delete mode 100644 bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrap.java delete mode 100644 bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/SessionIWrap.java delete mode 100644 bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/package-info.java delete mode 100644 bundle/src/test/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactoryTest.java delete mode 100644 bundle/src/test/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrapTest.java delete mode 100644 bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrapTest.java delete mode 100644 bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/SessionIWrapTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 571a10cd0e..fbb0c7b4eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) - #3140 - Fixed issue where malformed MCP process nodes can cause a NPE that breaks the entire MPC reporting UI. Now displays more friendly values in UI to help remove the invalid nodes. - #3150 - Support for case-insensitive redirect rules ( [NC] flag equivalent of apache) - #3138 - Re-arrange action removes data from redirect node + +## Removed + +- #3183 - Removed .wrap package including JackrabbitSessionIWrap and related classes which is no longer supported in Cloud Manager pipelines. ## 6.0.14 - 2023-07-11 diff --git a/bundle/src/main/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactory.java b/bundle/src/main/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactory.java deleted file mode 100644 index 7f8f1c3a78..0000000000 --- a/bundle/src/main/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactory.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.impl; - -import javax.annotation.Nonnull; -import javax.jcr.Session; - -import com.adobe.acs.commons.wrap.jackrabbit.JackrabbitSessionIWrap; -import com.adobe.acs.commons.wrap.jcr.SessionIWrap; -import org.apache.jackrabbit.api.JackrabbitSession; - -/** - * It's a factory, for session logout guards. - */ -public final class SessionLogoutGuardFactory { - - private SessionLogoutGuardFactory() { - /* no construction */ - } - - /** - * Wraps a {@link JackrabbitSession} and implements {@link JackrabbitSession}. - */ - static class JackrabbitWrapper implements JackrabbitSessionIWrap { - private final JackrabbitSession wrapped; - - JackrabbitWrapper(@Nonnull final JackrabbitSession wrapped) { - this.wrapped = wrapped; - } - - @Override - public void logout() { - /* prevent logout by doing nothing */ - } - - @Nonnull - @Override - public JackrabbitSession unwrapSession() { - return wrapped; - } - } - - /** - * Wraps a generic {@link Session} when it's not also a {@link JackrabbitSession}. - */ - static class JcrWrapper implements SessionIWrap { - private final Session wrapped; - - JcrWrapper(@Nonnull final Session wrapped) { - this.wrapped = wrapped; - } - - @Override - public void logout() { - /* prevent logout by doing nothing */ - } - - @Nonnull - @Override - public Session unwrapSession() { - return wrapped; - } - } - - /** - * Return the best wrapped version of the provided session. - * - * @param session the session to wrap - * @return a wrapped session - */ - public static Session useBestWrapper(final Session session) { - if (session instanceof JackrabbitWrapper || session instanceof JcrWrapper) { - return session; - } else if (session instanceof JackrabbitSession) { - return new JackrabbitWrapper((JackrabbitSession) session); - } else if (session != null) { - return new JcrWrapper(session); - } - return null; - } -} diff --git a/bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrap.java b/bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrap.java deleted file mode 100644 index ad23443a20..0000000000 --- a/bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrap.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.jackrabbit; - -import javax.annotation.Nonnull; -import javax.jcr.AccessDeniedException; -import javax.jcr.Item; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.UnsupportedRepositoryOperationException; - -import com.adobe.acs.commons.wrap.jcr.BaseSessionIWrap; -import org.apache.jackrabbit.api.JackrabbitSession; -import org.apache.jackrabbit.api.security.principal.PrincipalManager; -import org.apache.jackrabbit.api.security.user.UserManager; -import org.osgi.annotation.versioning.ProviderType; - -/** - * Interface for wrappers of JackrabbitSessions. - */ -@ProviderType -public interface JackrabbitSessionIWrap extends BaseSessionIWrap, JackrabbitSession { - - @Override - default boolean hasPermission(final @Nonnull String absPath, final @Nonnull String... actions) - throws RepositoryException { - return unwrapSession().hasPermission(absPath, actions); - } - - @Override - default PrincipalManager getPrincipalManager() - throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { - return unwrapSession().getPrincipalManager(); - } - - @Override - default UserManager getUserManager() - throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { - return unwrapSession().getUserManager(); - } - - @Override - default Item getItemOrNull(final String absPath) throws RepositoryException { - Item internal = unwrapSession().getItemOrNull(absPath); - return internal != null ? wrapItem(internal) : null; - } - - @Override - default Property getPropertyOrNull(final String absPath) throws RepositoryException { - Property internal = unwrapSession().getPropertyOrNull(absPath); - return internal != null ? wrapItem(internal) : null; - } - - @Override - default Node getNodeOrNull(final String absPath) throws RepositoryException { - Node internal = unwrapSession().getNodeOrNull(absPath); - return internal != null ? wrapItem(internal) : null; - } -} diff --git a/bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/package-info.java b/bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/package-info.java deleted file mode 100644 index ead2b1ff36..0000000000 --- a/bundle/src/main/java/com/adobe/acs/commons/wrap/jackrabbit/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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. - */ -/** - * Http Injectors. - */ -@org.osgi.annotation.versioning.Version("4.0.0") -package com.adobe.acs.commons.wrap.jackrabbit; \ No newline at end of file diff --git a/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrap.java b/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrap.java deleted file mode 100644 index fe09178ad6..0000000000 --- a/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrap.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.jcr; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.AccessControlException; -import javax.annotation.Nonnull; -import javax.jcr.AccessDeniedException; -import javax.jcr.Credentials; -import javax.jcr.InvalidItemStateException; -import javax.jcr.InvalidSerializedDataException; -import javax.jcr.Item; -import javax.jcr.ItemExistsException; -import javax.jcr.ItemNotFoundException; -import javax.jcr.LoginException; -import javax.jcr.NamespaceException; -import javax.jcr.Node; -import javax.jcr.PathNotFoundException; -import javax.jcr.Property; -import javax.jcr.ReferentialIntegrityException; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.UnsupportedRepositoryOperationException; -import javax.jcr.ValueFactory; -import javax.jcr.Workspace; -import javax.jcr.lock.LockException; -import javax.jcr.nodetype.ConstraintViolationException; -import javax.jcr.nodetype.NoSuchNodeTypeException; -import javax.jcr.retention.RetentionManager; -import javax.jcr.security.AccessControlManager; -import javax.jcr.version.VersionException; - -import org.osgi.annotation.versioning.ProviderType; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; - -/** - * This is the base interface for {@link SessionIWrap} and - * {@link com.adobe.acs.commons.wrap.jackrabbit.JackrabbitSessionIWrap}. Default methods are not only defined for all - * {@link Session} methods, but also for a few methods to wrap other JCR types that retain references to the wrapped - * session, and which therefore might need to be overridden to keep the integrity of the wrapper API in more complex use - * cases. - * - * @param the type of session that is being wrapped. - */ -@ProviderType -public interface BaseSessionIWrap extends Session { - - /** - * Return the underlying session. - * - * @return the underlying session - */ - S unwrapSession(); - - default @Nonnull - T wrapItem(final @Nonnull T item) { - return item; - } - - default @Nonnull - Session wrapSession(final @Nonnull Session session) { - return session; - } - - default @Nonnull - Workspace wrapWorkspace(final @Nonnull Workspace workspace) { - return workspace; - } - - @Override - default Repository getRepository() { - return unwrapSession().getRepository(); - } - - @Override - default String getUserID() { - return unwrapSession().getUserID(); - } - - @Override - default String[] getAttributeNames() { - return unwrapSession().getAttributeNames(); - } - - @Override - default Object getAttribute(final String name) { - return unwrapSession().getAttribute(name); - } - - @Override - default Workspace getWorkspace() { - return wrapWorkspace(unwrapSession().getWorkspace()); - } - - @Override - default Node getRootNode() throws RepositoryException { - return unwrapSession().getRootNode(); - } - - @Override - default Session impersonate(final Credentials credentials) throws LoginException, RepositoryException { - return wrapSession(unwrapSession().impersonate(credentials)); - } - - @SuppressWarnings("deprecation") - @Override - default Node getNodeByUUID(final String uuid) throws ItemNotFoundException, RepositoryException { - return wrapItem(unwrapSession().getNodeByUUID(uuid)); - } - - @Override - default Node getNodeByIdentifier(final String id) throws ItemNotFoundException, RepositoryException { - return wrapItem(unwrapSession().getNodeByIdentifier(id)); - } - - @Override - default Item getItem(final String absPath) throws PathNotFoundException, RepositoryException { - return wrapItem(unwrapSession().getItem(absPath)); - } - - @Override - default Node getNode(final String absPath) throws PathNotFoundException, RepositoryException { - return wrapItem(unwrapSession().getNode(absPath)); - } - - @Override - default Property getProperty(final String absPath) throws PathNotFoundException, RepositoryException { - return wrapItem(unwrapSession().getProperty(absPath)); - } - - @Override - default boolean itemExists(final String absPath) throws RepositoryException { - return unwrapSession().itemExists(absPath); - } - - @Override - default boolean nodeExists(final String absPath) throws RepositoryException { - return unwrapSession().nodeExists(absPath); - } - - @Override - default boolean propertyExists(final String absPath) throws RepositoryException { - return unwrapSession().propertyExists(absPath); - } - - @Override - default void move(final String srcAbsPath, final String destAbsPath) - throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, - LockException, RepositoryException { - unwrapSession().move(srcAbsPath, destAbsPath); - } - - @Override - default void removeItem(final String absPath) - throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, - RepositoryException { - unwrapSession().removeItem(absPath); - } - - @Override - default void save() - throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, - ConstraintViolationException, InvalidItemStateException, VersionException, LockException, - NoSuchNodeTypeException, RepositoryException { - unwrapSession().save(); - } - - @Override - default void refresh(final boolean keepChanges) throws RepositoryException { - unwrapSession().refresh(keepChanges); - } - - @Override - default boolean hasPendingChanges() throws RepositoryException { - return unwrapSession().hasPendingChanges(); - } - - @Override - default ValueFactory getValueFactory() throws UnsupportedRepositoryOperationException, RepositoryException { - return unwrapSession().getValueFactory(); - } - - @Override - default boolean hasPermission(final String absPath, final String actions) throws RepositoryException { - return unwrapSession().hasPermission(absPath, actions); - } - - @Override - default void checkPermission(final String absPath, final String actions) throws AccessControlException, - RepositoryException { - unwrapSession().checkPermission(absPath, actions); - } - - @Override - default boolean hasCapability(final String methodName, final Object target, final Object[] arguments) - throws RepositoryException { - return unwrapSession().hasCapability(methodName, target, arguments); - } - - @Override - default ContentHandler getImportContentHandler(final String parentAbsPath, final int uuidBehavior) - throws PathNotFoundException, ConstraintViolationException, VersionException, LockException, - RepositoryException { - return unwrapSession().getImportContentHandler(parentAbsPath, uuidBehavior); - } - - @Override - default void importXML(final String parentAbsPath, final InputStream in, final int uuidBehavior) - throws IOException, PathNotFoundException, ItemExistsException, ConstraintViolationException, - VersionException, InvalidSerializedDataException, LockException, RepositoryException { - unwrapSession().importXML(parentAbsPath, in, uuidBehavior); - } - - @Override - default void exportSystemView(final String absPath, - final ContentHandler contentHandler, - final boolean skipBinary, - final boolean noRecurse) - throws PathNotFoundException, SAXException, RepositoryException { - unwrapSession().exportSystemView(absPath, contentHandler, skipBinary, noRecurse); - } - - @Override - default void exportSystemView(final String absPath, - final OutputStream out, - final boolean skipBinary, - final boolean noRecurse) - throws IOException, PathNotFoundException, RepositoryException { - unwrapSession().exportSystemView(absPath, out, skipBinary, noRecurse); - } - - @Override - default void exportDocumentView(final String absPath, - final ContentHandler contentHandler, - final boolean skipBinary, - final boolean noRecurse) - throws PathNotFoundException, SAXException, RepositoryException { - unwrapSession().exportDocumentView(absPath, contentHandler, skipBinary, noRecurse); - } - - @Override - default void exportDocumentView(final String absPath, - final OutputStream out, - final boolean skipBinary, - final boolean noRecurse) - throws IOException, PathNotFoundException, RepositoryException { - unwrapSession().exportDocumentView(absPath, out, skipBinary, noRecurse); - } - - @Override - default void setNamespacePrefix(final String prefix, - final String uri) throws NamespaceException, RepositoryException { - unwrapSession().setNamespacePrefix(prefix, uri); - } - - @Override - default String[] getNamespacePrefixes() throws RepositoryException { - return unwrapSession().getNamespacePrefixes(); - } - - @Override - default String getNamespaceURI(final String prefix) throws NamespaceException, RepositoryException { - return unwrapSession().getNamespaceURI(prefix); - } - - @Override - default String getNamespacePrefix(final String uri) throws NamespaceException, RepositoryException { - return unwrapSession().getNamespacePrefix(uri); - } - - @Override - default void logout() { - unwrapSession().logout(); - } - - @Override - default boolean isLive() { - return unwrapSession().isLive(); - } - - @SuppressWarnings("deprecation") - @Override - default void addLockToken(final String lt) { - unwrapSession().addLockToken(lt); - } - - @SuppressWarnings("deprecation") - @Override - default String[] getLockTokens() { - return unwrapSession().getLockTokens(); - } - - @SuppressWarnings("deprecation") - @Override - default void removeLockToken(final String lt) { - unwrapSession().removeLockToken(lt); - } - - @Override - default AccessControlManager getAccessControlManager() - throws UnsupportedRepositoryOperationException, RepositoryException { - return unwrapSession().getAccessControlManager(); - } - - @Override - default RetentionManager getRetentionManager() - throws UnsupportedRepositoryOperationException, RepositoryException { - return unwrapSession().getRetentionManager(); - } -} diff --git a/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/SessionIWrap.java b/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/SessionIWrap.java deleted file mode 100644 index ed79224823..0000000000 --- a/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/SessionIWrap.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.jcr; - -import javax.jcr.Session; - -import org.osgi.annotation.versioning.ProviderType; - -/** - * Terminal interface for wrappers of generic JCR Session objects. - */ -@ProviderType -public interface SessionIWrap extends BaseSessionIWrap { - -} diff --git a/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/package-info.java b/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/package-info.java deleted file mode 100644 index 6580ea1e5a..0000000000 --- a/bundle/src/main/java/com/adobe/acs/commons/wrap/jcr/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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. - */ -/** - * Http Injectors. - */ -@org.osgi.annotation.versioning.Version("4.0.0") -package com.adobe.acs.commons.wrap.jcr; \ No newline at end of file diff --git a/bundle/src/test/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactoryTest.java b/bundle/src/test/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactoryTest.java deleted file mode 100644 index c9c3ae9fad..0000000000 --- a/bundle/src/test/java/com/adobe/acs/commons/wrap/impl/SessionLogoutGuardFactoryTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.impl; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import javax.jcr.Session; - -import com.adobe.acs.commons.wrap.impl.SessionLogoutGuardFactory; -import com.adobe.acs.commons.wrap.jackrabbit.JackrabbitSessionIWrap; -import com.adobe.acs.commons.wrap.jcr.SessionIWrap; -import org.apache.jackrabbit.api.JackrabbitSession; -import org.junit.Test; - -public class SessionLogoutGuardFactoryTest { - - @Test - public void testUseBestWrapper() { - assertNull("null should pass through", SessionLogoutGuardFactory.useBestWrapper(null)); - - JackrabbitSession mockJackSession = mock(JackrabbitSession.class); - Session jackWrapper = SessionLogoutGuardFactory.useBestWrapper(mockJackSession); - assertNotNull("wrap jackSession should not be null", jackWrapper); - assertTrue("jackWrapper instance of JackrabbitSessionIWrap: " + jackWrapper.getClass().getName(), - jackWrapper instanceof JackrabbitSessionIWrap); - assertNotNull("jackWrapper.unwrapSession() instanceof JackrabbitSession", - ((JackrabbitSessionIWrap) jackWrapper).unwrapSession()); - - jackWrapper.logout(); - verify(mockJackSession, times(0)).logout(); - - assertSame("jackWrapper should not get rewrapped", - jackWrapper, SessionLogoutGuardFactory.useBestWrapper(jackWrapper)); - - Session mockJcrSession = mock(Session.class); - Session jcrWrapper = SessionLogoutGuardFactory.useBestWrapper(mockJcrSession); - assertNotNull("wrap jcrSession should not be null", jcrWrapper); - assertTrue("jcrWrapper instance of SessionIWrap: " + jcrWrapper.getClass().getName(), - jcrWrapper instanceof SessionIWrap); - assertNotNull("jcrWrapper.unwrapSession() instanceof Session", - ((SessionIWrap) jcrWrapper).unwrapSession()); - - jcrWrapper.logout(); - verify(mockJcrSession, times(0)).logout(); - - assertSame("jcrWrapper should not get rewrapped", - jcrWrapper, SessionLogoutGuardFactory.useBestWrapper(jcrWrapper)); - } -} diff --git a/bundle/src/test/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrapTest.java b/bundle/src/test/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrapTest.java deleted file mode 100644 index 61b651daf5..0000000000 --- a/bundle/src/test/java/com/adobe/acs/commons/wrap/jackrabbit/JackrabbitSessionIWrapTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.jackrabbit; - -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import javax.jcr.Node; -import javax.jcr.Property; - -import com.adobe.acs.commons.wrap.jackrabbit.JackrabbitSessionIWrap; -import org.apache.jackrabbit.api.JackrabbitSession; -import org.apache.jackrabbit.api.security.principal.PrincipalManager; -import org.apache.jackrabbit.api.security.user.UserManager; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class JackrabbitSessionIWrapTest { - - @Mock - JackrabbitSession session; - - @Mock - UserManager userManager; - - @Mock - PrincipalManager principalManager; - - @Mock - Node mockNode; - - @Mock - Property mockProperty; - - @Before - public void setUp() throws Exception { - when(session.getPrincipalManager()).thenReturn(principalManager); - when(session.getUserManager()).thenReturn(userManager); - when(session.hasPermission(anyString(), anyString(), anyString())).thenReturn(true); - when(session.getItemOrNull("/item/null")).thenReturn(null); - when(session.getItemOrNull("/item/node")).thenReturn(mockNode); - when(session.getItemOrNull("/item/property")).thenReturn(mockProperty); - when(session.getNodeOrNull("/item/null")).thenReturn(null); - when(session.getNodeOrNull("/item/node")).thenReturn(mockNode); - when(session.getPropertyOrNull("/item/null")).thenReturn(null); - when(session.getPropertyOrNull("/item/property")).thenReturn(mockProperty); - } - - @Test - public void testWrapper() throws Exception { - JackrabbitSession wrapper = new JackrabbitSessionWrapper(session); - assertSame("UserManager should be same as mocked", userManager, wrapper.getUserManager()); - assertSame("PrincipalManager should be same as mocked", principalManager, wrapper.getPrincipalManager()); - assertTrue("hasPermission should return true and be counted.", - wrapper.hasPermission("/path", "perm1", "perm2")); - - assertNull("getItemOrNull(/item/null) should return null", wrapper.getItemOrNull("/item/null")); - assertNull("getNodeOrNull(/item/null) should return null", wrapper.getNodeOrNull("/item/null")); - assertNull("getPropertyOrNull(/item/null) should return null", wrapper.getPropertyOrNull("/item/null")); - - verify(session, times(1)).getItemOrNull("/item/null"); - verify(session, times(1)).getNodeOrNull("/item/null"); - verify(session, times(1)).getPropertyOrNull("/item/null"); - - assertSame("getItemOrNull(/item/node) should return mockNode", mockNode, wrapper.getItemOrNull("/item/node")); - assertSame("getItemOrNull(/item/property) should return mockProperty", mockProperty, wrapper.getItemOrNull("/item/property")); - - verify(session, times(1)).getItemOrNull("/item/node"); - verify(session, times(1)).getItemOrNull("/item/property"); - - assertSame("getNodeOrNull(/item/node) should return mockNode", mockNode, wrapper.getNodeOrNull("/item/node")); - assertSame("getPropertyOrNull(/item/property) should return mockProperty", mockProperty, wrapper.getPropertyOrNull("/item/property")); - - verify(session, times(1)).getNodeOrNull("/item/node"); - verify(session, times(1)).getPropertyOrNull("/item/property"); - } - - static class JackrabbitSessionWrapper implements JackrabbitSessionIWrap { - final JackrabbitSession session; - - public JackrabbitSessionWrapper(final JackrabbitSession session) { - this.session = session; - } - - @Override - public JackrabbitSession unwrapSession() { - return session; - } - } -} diff --git a/bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrapTest.java b/bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrapTest.java deleted file mode 100644 index fe49a16ac4..0000000000 --- a/bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/BaseSessionIWrapTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.jcr; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.OutputStream; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.Session; -import javax.jcr.ValueFactory; -import javax.jcr.Workspace; -import javax.jcr.retention.RetentionManager; -import javax.jcr.security.AccessControlManager; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.xml.sax.ContentHandler; - -@RunWith(MockitoJUnitRunner.class) -@SuppressWarnings("deprecation") -public class BaseSessionIWrapTest { - - @Mock - Session session; - - @Mock - Session otherSession; - - @Mock - Workspace workspace; - - @Mock - Repository repository; - - @Mock - Node rootNode; - - @Mock - Property someProperty; - - @Mock - ValueFactory valueFactory; - - @Mock - ContentHandler contentHandler; - - @Mock - AccessControlManager accessControlManager; - - @Mock - RetentionManager retentionManager; - - String[] attributeNames = new String[]{"attrOne", "attrTwo"}; - String[] namespacePrefixes = new String[]{"jcr", "nt", "sling"}; - String[] lockTokens = new String[]{"lockOne", "lockTwo"}; - - @Before - public void setUp() throws Exception { - when(session.getRootNode()).thenReturn(rootNode); - when(session.getWorkspace()).thenReturn(workspace); - when(session.getRepository()).thenReturn(repository); - when(session.getUserID()).thenReturn("hal9000"); - when(session.getAttributeNames()).thenReturn(attributeNames); - when(session.getAttribute("attrOne")).thenReturn("valueOne"); - when(session.impersonate(null)).thenReturn(otherSession); - when(session.getNodeByUUID(anyString())).thenReturn(rootNode); - when(session.getNodeByIdentifier(anyString())).thenReturn(rootNode); - when(session.getItem("/item/node")).thenReturn(rootNode); - when(session.getNode("/item/node")).thenReturn(rootNode); - when(session.getProperty("/item/property")).thenReturn(someProperty); - when(session.itemExists(anyString())).thenReturn(true); - when(session.nodeExists(anyString())).thenReturn(true); - when(session.propertyExists(anyString())).thenReturn(true); - - when(session.hasPendingChanges()).thenReturn(true); - when(session.getValueFactory()).thenReturn(valueFactory); - when(session.hasPermission(anyString(), anyString())).thenReturn(true); - when(session.hasCapability(anyString(), any(), any(Object[].class))).thenReturn(true); - when(session.getImportContentHandler(anyString(), anyInt())).thenReturn(contentHandler); - when(session.getNamespacePrefixes()).thenReturn(namespacePrefixes); - when(session.getNamespaceURI(anyString())).thenReturn("http://someuri/"); - when(session.getNamespacePrefix(anyString())).thenReturn("someprefix"); - when(session.isLive()).thenReturn(true); - when(session.getLockTokens()).thenReturn(lockTokens); - when(session.getAccessControlManager()).thenReturn(accessControlManager); - when(session.getRetentionManager()).thenReturn(retentionManager); - } - - @Test - public void testGets() throws Exception { - SessionWrapper wrapper = new SessionWrapper(session); - assertSame("wrapItem(rootNode) should be same as mock", rootNode, wrapper.wrapItem(rootNode)); - assertSame("wrapWorkspace(workspace) should be same as mock", workspace, wrapper.wrapWorkspace(workspace)); - assertSame("wrapSession(session) should be same as mock", otherSession, wrapper.wrapSession(otherSession)); - assertSame("root node should be same as mock", rootNode, wrapper.getRootNode()); - assertSame("repository should be same as mock", repository, wrapper.getRepository()); - assertEquals("expect correct userId", "hal9000", wrapper.getUserID()); - assertArrayEquals("expect correct attributeNames", attributeNames, wrapper.getAttributeNames()); - assertEquals("expect correct attribute", "valueOne", wrapper.getAttribute("attrOne")); - assertSame("expect same workspace", workspace, wrapper.getWorkspace()); - assertSame("expect other session on impersonate", otherSession, wrapper.impersonate(null)); - assertSame("get root node by uuid", rootNode, wrapper.getNodeByUUID("some-uuid")); - assertSame("get root node by identifier", rootNode, wrapper.getNodeByIdentifier("some-uuid")); - assertSame("getItem()", rootNode, wrapper.getItem("/item/node")); - assertSame("getNode()", rootNode, wrapper.getNode("/item/node")); - assertSame("getProperty()", someProperty, wrapper.getProperty("/item/property")); - assertTrue("itemExists", wrapper.itemExists("/item/node")); - assertTrue("nodeExists", wrapper.nodeExists("/item/node")); - assertTrue("propertyExists", wrapper.propertyExists("/item/property")); - assertTrue("hasPendingChanges", wrapper.hasPendingChanges()); - assertSame("getValueFactory", valueFactory, wrapper.getValueFactory()); - assertTrue("hasPermission", wrapper.hasPermission("path", "action")); - assertTrue("hasCapability", wrapper.hasCapability("path", "action", new Object[0])); - assertSame("getImportContentHandler", contentHandler, wrapper.getImportContentHandler("path", 0)); - assertArrayEquals("expect correct namespacePrefixes", namespacePrefixes, wrapper.getNamespacePrefixes()); - assertEquals("getNamespaceURI", "http://someuri/", wrapper.getNamespaceURI("someprefix")); - assertEquals("getNamespacePrefix", "someprefix", wrapper.getNamespacePrefix("http://someuri/")); - assertTrue("isLive", wrapper.isLive()); - assertArrayEquals("getLockTokens", lockTokens, wrapper.getLockTokens()); - assertSame("getAccessControlManager", accessControlManager, wrapper.getAccessControlManager()); - assertSame("getRetentionManager", retentionManager, wrapper.getRetentionManager()); - } - - @Test - public void testVoids() throws Exception { - SessionWrapper wrapper = new SessionWrapper(session); - wrapper.move("/from", "/to"); - verify(session, times(1)).move("/from", "/to"); - wrapper.removeItem("/item"); - verify(session, times(1)).removeItem("/item"); - wrapper.save(); - verify(session, times(1)).save(); - wrapper.refresh(true); - verify(session, times(1)).refresh(true); - wrapper.refresh(false); - verify(session, times(1)).refresh(false); - wrapper.checkPermission("/path", "actions"); - verify(session, times(1)).checkPermission("/path", "actions"); - wrapper.importXML("/path", null, 42); - - final OutputStream mockOs = mock(OutputStream.class); - verify(session, times(1)).importXML("/path", null, 42); - wrapper.exportSystemView("/path", contentHandler, true, false); - verify(session, times(1)).exportSystemView("/path", contentHandler, true, false); - wrapper.exportSystemView("/path", contentHandler, false, true); - verify(session, times(1)).exportSystemView("/path", contentHandler, false, true); - wrapper.exportSystemView("/pathos", mockOs, true, false); - verify(session, times(1)).exportSystemView("/pathos", mockOs, true, false); - wrapper.exportSystemView("/pathos", mockOs, false, true); - verify(session, times(1)).exportSystemView("/pathos", mockOs, false, true); - wrapper.exportDocumentView("/path", contentHandler, true, false); - verify(session, times(1)).exportDocumentView("/path", contentHandler, true, false); - wrapper.exportDocumentView("/path", contentHandler, false, true); - verify(session, times(1)).exportDocumentView("/path", contentHandler, false, true); - wrapper.exportDocumentView("/pathos", mockOs, true, false); - verify(session, times(1)).exportDocumentView("/pathos", mockOs, true, false); - wrapper.exportDocumentView("/pathos", mockOs, false, true); - verify(session, times(1)).exportDocumentView("/pathos", mockOs, false, true); - - wrapper.setNamespacePrefix("someprefix", "http://someuri/"); - verify(session, times(1)).setNamespacePrefix("someprefix", "http://someuri/"); - - wrapper.logout(); - verify(session, times(1)).logout(); - - wrapper.addLockToken("lockToken"); - verify(session, times(1)).addLockToken("lockToken"); - wrapper.removeLockToken("lockToken"); - verify(session, times(1)).removeLockToken("lockToken"); - - } - - static class SessionWrapper implements BaseSessionIWrap { - final Session wrapped; - - public SessionWrapper(final Session wrapped) { - this.wrapped = wrapped; - } - - @Override - public Session unwrapSession() { - return wrapped; - } - } -} diff --git a/bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/SessionIWrapTest.java b/bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/SessionIWrapTest.java deleted file mode 100644 index c898b171a5..0000000000 --- a/bundle/src/test/java/com/adobe/acs/commons/wrap/jcr/SessionIWrapTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ACS AEM Commons - * - * Copyright (C) 2013 - 2023 Adobe - * - * 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.adobe.acs.commons.wrap.jcr; - -import static org.junit.Assert.assertSame; - -import javax.jcr.Session; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class SessionIWrapTest { - - @Mock - Session mockSession; - - @Test - public void testConstructImpl() { - SessionWrapper wrapper = new SessionWrapper(mockSession); - assertSame("unwrapSession to same as mock", mockSession, wrapper.unwrapSession()); - } - - static class SessionWrapper implements SessionIWrap { - final Session wrapped; - - public SessionWrapper(final Session wrapped) { - this.wrapped = wrapped; - } - - @Override - public Session unwrapSession() { - return wrapped; - } - } -} From 197df67f4a032e051e04ca3b993c3a989b441792 Mon Sep 17 00:00:00 2001 From: Joseph Rignanese Date: Fri, 15 Sep 2023 00:28:43 +1000 Subject: [PATCH 08/10] Issue 3166 - New option to suppress status update in replication workflow processes (#3182) * 3166 - Added suppressStatusUpdate option --------- Co-authored-by: david g --- CHANGELOG.md | 1 + .../ParameterizedActivatePageProcess.java | 4 ++ .../ParameterizedDeactivatePageProcess.java | 4 ++ .../ReplicateWithOptionsWorkflowProcess.java | 2 + ...plicateWithOptionsWorkflowProcessTest.java | 55 +++++++++++++++++++ .../select-agent/_cq_dialog/.content.xml | 8 +++ 6 files changed, 74 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb0c7b4eb..bba7095e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) - #3159 - Add PageProperty annotation for Sling Models - #3170 - Added a new MCP tool to bulk tag AEM content pages via an Excel file input. +- #3166 - New option to suppress status updates in replication workflow processes ## Fixed diff --git a/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedActivatePageProcess.java b/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedActivatePageProcess.java index e0aacc6125..130825a53b 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedActivatePageProcess.java +++ b/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedActivatePageProcess.java @@ -51,12 +51,15 @@ public class ParameterizedActivatePageProcess extends ActivatePageProcess { private static final String AGENT_ARG = "replicationAgent"; + private static final String ARG_REPLICATION_SUPPRESS_STATUS_UPDATE = "suppressStatusUpdate"; private transient ThreadLocal agentId = new ThreadLocal(); + private boolean suppressStatusUpdate = false; @Override public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) throws WorkflowException { agentId.set(args.get(AGENT_ARG, new String[] {})); + this.suppressStatusUpdate = Boolean.parseBoolean(args.get(ARG_REPLICATION_SUPPRESS_STATUS_UPDATE, String.class)); super.execute(workItem, workflowSession, args); } @@ -78,6 +81,7 @@ public boolean isIncluded(Agent agent) { return ArrayUtils.contains(agentId.get(), agent.getId()); } }); + opts.setSuppressStatusUpdate(suppressStatusUpdate); return opts; } diff --git a/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedDeactivatePageProcess.java b/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedDeactivatePageProcess.java index 9ded77fbec..d6b5d6220d 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedDeactivatePageProcess.java +++ b/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ParameterizedDeactivatePageProcess.java @@ -51,12 +51,15 @@ public class ParameterizedDeactivatePageProcess extends DeactivatePageProcess { private static final String AGENT_ARG = "replicationAgent"; + private static final String ARG_REPLICATION_SUPPRESS_STATUS_UPDATE = "suppressStatusUpdate"; private transient ThreadLocal agentId = new ThreadLocal(); + private boolean suppressStatusUpdate = false; @Override public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) throws WorkflowException { agentId.set(args.get(AGENT_ARG, new String[] {})); + this.suppressStatusUpdate = Boolean.parseBoolean(args.get(ARG_REPLICATION_SUPPRESS_STATUS_UPDATE, String.class)); super.execute(workItem, workflowSession, args); } @@ -78,6 +81,7 @@ public boolean isIncluded(Agent agent) { return ArrayUtils.contains(agentId.get(), agent.getId()); } }); + opts.setSuppressStatusUpdate(suppressStatusUpdate); return opts; } diff --git a/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcess.java b/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcess.java index f1a7b90540..ac9303dde1 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcess.java +++ b/bundle/src/main/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcess.java @@ -72,6 +72,7 @@ public class ReplicateWithOptionsWorkflowProcess implements WorkflowProcess { private static final String ARG_TRAVERSE_TREE = "traverseTree"; private static final String ARG_REPLICATION_ACTION_TYPE = "replicationActionType"; private static final String ARG_REPLICATION_SYNCHRONOUS = "synchronous"; + private static final String ARG_REPLICATION_SUPPRESS_STATUS_UPDATE = "suppressStatusUpdate"; private static final String ARG_REPLICATION_SUPPRESS_VERSIONS = "suppressVersions"; private static final String ARG_THROTTLE = "throttle"; private static final String ARG_AGENTS = "agents"; @@ -160,6 +161,7 @@ public ProcessArgs(MetaDataMap map) throws WorkflowException { } replicationOptions.setSynchronous(Boolean.parseBoolean(data.get(ARG_REPLICATION_SYNCHRONOUS))); replicationOptions.setSuppressVersions(Boolean.parseBoolean(data.get(ARG_REPLICATION_SUPPRESS_VERSIONS))); + replicationOptions.setSuppressStatusUpdate(Boolean.parseBoolean(data.get(ARG_REPLICATION_SUPPRESS_STATUS_UPDATE))); agents = Arrays.asList(StringUtils.split(data.get(ARG_AGENTS), ",")); } diff --git a/bundle/src/test/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcessTest.java b/bundle/src/test/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcessTest.java index ad5160f5d6..d8338ac05b 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcessTest.java +++ b/bundle/src/test/java/com/adobe/acs/commons/workflow/process/impl/ReplicateWithOptionsWorkflowProcessTest.java @@ -200,10 +200,57 @@ public void testExecuteBrandPortal() throws Exception { verifyNoInteractions(throttledTaskRunner); } + @Test + public void testExecuteDeactivateWithSupressStatusUpdate() throws Exception { + when(workflowPackageManager.getPaths(context.resourceResolver(), "/content/payload")) + .thenReturn(Arrays.asList("/content/page", "/content/asset")); + + StringBuilder args = new StringBuilder(); + args.append("replicationActionType=Deactivate"); + args.append(System.lineSeparator()); + args.append("agents=agent1"); + args.append(System.lineSeparator()); + args.append("suppressStatusUpdate=true"); + + when(metaDataMap.get(WorkflowHelper.PROCESS_ARGS, "")).thenReturn(args.toString()); + process.execute(workItem, workflowSession, metaDataMap); + + ReplicationOptionsMatcher optionsMatcher = new ReplicationOptionsMatcher().withSuppressStatusUpdate(true); + + verify(replicator).replicate(any(), eq(ReplicationActionType.DEACTIVATE), eq("/content/page"), argThat(optionsMatcher)); + verify(replicator).replicate(any(), eq(ReplicationActionType.DEACTIVATE), eq("/content/asset"), argThat(optionsMatcher)); + verifyNoMoreInteractions(replicator); + verifyNoInteractions(throttledTaskRunner); + } + + @Test + public void testExecuteDeactivateWithSupressStatusUpdateFalse() throws Exception { + when(workflowPackageManager.getPaths(context.resourceResolver(), "/content/payload")) + .thenReturn(Arrays.asList("/content/page", "/content/asset")); + + StringBuilder args = new StringBuilder(); + args.append("replicationActionType=Deactivate"); + args.append(System.lineSeparator()); + args.append("agents=agent1"); + args.append(System.lineSeparator()); + args.append("suppressStatusUpdate=false"); + + when(metaDataMap.get(WorkflowHelper.PROCESS_ARGS, "")).thenReturn(args.toString()); + process.execute(workItem, workflowSession, metaDataMap); + + ReplicationOptionsMatcher optionsMatcher = new ReplicationOptionsMatcher().withSuppressStatusUpdate(false); + + verify(replicator).replicate(any(), eq(ReplicationActionType.DEACTIVATE), eq("/content/page"), argThat(optionsMatcher)); + verify(replicator).replicate(any(), eq(ReplicationActionType.DEACTIVATE), eq("/content/asset"), argThat(optionsMatcher)); + verifyNoMoreInteractions(replicator); + verifyNoInteractions(throttledTaskRunner); + } + private static class ReplicationOptionsMatcher implements ArgumentMatcher { private String filterAgentId; private boolean brandPortalFilter; + private boolean suppressStatusUpdate; public ReplicationOptionsMatcher withAgentIdFilter(String filterAgentId) { this.filterAgentId = filterAgentId; @@ -215,6 +262,11 @@ public ReplicationOptionsMatcher withBrandPortalFilter() { return this; } + public ReplicationOptionsMatcher withSuppressStatusUpdate(boolean suppressStatusUpdate) { + this.suppressStatusUpdate = suppressStatusUpdate; + return this; + } + @Override public boolean matches(ReplicationOptions argument) { ReplicationOptions options = (ReplicationOptions) argument; @@ -227,6 +279,9 @@ public boolean matches(ReplicationOptions argument) { if (brandPortalFilter) { matches = matches && options.getFilter() instanceof BrandPortalAgentFilter; } + if (suppressStatusUpdate) { + matches = matches && options.isSuppressStatusUpdate(); + } return matches; } diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/workflow/select-agent/_cq_dialog/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/workflow/select-agent/_cq_dialog/.content.xml index 5dd1524b99..35a2596732 100644 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/workflow/select-agent/_cq_dialog/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/workflow/select-agent/_cq_dialog/.content.xml @@ -120,6 +120,14 @@ jcr:primaryType="nt:unstructured" sling:resourceType="acs-commons/components/workflow/select-agent/datasource"/> + From 04dd3674fbd03727c16542a84c08339be52cac8e Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Thu, 14 Sep 2023 16:29:34 +0200 Subject: [PATCH 09/10] Feature: Content Sync (#3151) * new feature: content sync --- CHANGELOG.md | 3 + bundle/pom.xml | 6 + .../acs/commons/contentsync/CatalogItem.java | 74 ++ .../contentsync/ConfigurationUtils.java | 60 + .../commons/contentsync/ContentCatalog.java | 84 ++ .../commons/contentsync/ContentReader.java | 214 +++ .../acs/commons/contentsync/ContentSync.java | 314 +++++ .../commons/contentsync/RemoteInstance.java | 176 +++ .../contentsync/SyncHostConfiguration.java | 54 + .../commons/contentsync/UpdateStrategy.java | 54 + .../impl/AssetChecksumStrategy.java | 123 ++ .../impl/LastModifiedStrategy.java | 264 ++++ .../acs/commons/contentsync/package-info.java | 21 + .../servlet/ContentCatalogServlet.java | 109 ++ .../contentsync/TestContentReader.java | 221 ++++ .../commons/contentsync/TestContentSync.java | 408 ++++++ .../impl/TestLastModifiedStrategy.java | 202 +++ .../servlet/TestContentCatalogServlet.java | 151 +++ .../src/test/resources/contentsync/asset.json | 1158 +++++++++++++++++ .../resources/contentsync/jcr_content.json | 107 ++ .../resources/contentsync/ordered-folder.json | 8 + .../riverside-camping-australia.1.json | 115 ++ .../test/resources/contentsync/wknd-faqs.json | 301 +++++ .../components/utilities/contentsync/POST.jsp | 254 ++++ .../contentsync/clientlibs/.content.xml | 5 + .../utilities/contentsync/clientlibs/css.txt | 3 + .../contentsync/clientlibs/css/app.css | 13 + .../utilities/contentsync/clientlibs/js.txt | 3 + .../contentsync/clientlibs/js/app.js | 72 + .../configurehostdatasource.jsp | 38 + .../configurehostentry/configurehostentry.jsp | 73 ++ .../utilities/contentsync/iframe/.content.xml | 3 + .../utilities/contentsync/iframe/iframe.jsp | 36 + .../selecthostdatasource.jsp | 51 + .../strategydatasource/strategydatasource.jsp | 44 + .../content/contentsync/.content.xml | 130 ++ .../contentsync/configure/.content.xml | 275 ++++ .../nav/tools/acs-commons/.content.xml | 1 + .../acs-commons/contentsync/.content.xml | 27 + 39 files changed, 5255 insertions(+) create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java create mode 100644 bundle/src/test/resources/contentsync/asset.json create mode 100644 bundle/src/test/resources/contentsync/jcr_content.json create mode 100644 bundle/src/test/resources/contentsync/ordered-folder.json create mode 100644 bundle/src/test/resources/contentsync/riverside-camping-australia.1.json create mode 100644 bundle/src/test/resources/contentsync/wknd-faqs.json create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml create mode 100644 ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index bba7095e22..b0a67eb18e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) ## Unreleased ([details][unreleased changes details]) +- #3151 - New ContentSync utility +- #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet + ## 6.1.0 - 2023-09-08 ## Added diff --git a/bundle/pom.xml b/bundle/pom.xml index c0d2b8c9ec..da16bdf0b2 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -736,6 +736,12 @@ gson test + + org.apache.sling + org.apache.sling.jcr.contentloader + 2.2.6 + test + diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java new file mode 100644 index 0000000000..cace9378bd --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java @@ -0,0 +1,74 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + + +import javax.json.JsonObject; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; + +/** + * A Json object describing a resource to sync. + * + * Required fields: + * - path + * - jcr:primaryType + * - exportUri + */ +public class CatalogItem { + private final JsonObject object; + + public CatalogItem(JsonObject object){ + this.object = object; + } + + public String getPath(){ + return object.getString("path"); + } + + public String getPrimaryType(){ + return object.getString("jcr:primaryType"); + } + + public boolean hasContentResource(){ + return getContentUri().endsWith("/" + JCR_CONTENT + ".infinity.json"); + } + + public String getContentUri(){ + return object.getString("exportUri"); + } + + public String getString(String key){ + return object.containsKey(key) ? object.getString(key) : null; + } + + public long getLong(String key){ + return object.containsKey(key) ? object.getJsonNumber(key).longValue() : 0L; + } + + public String getCustomExporter(){ + return object.containsKey("renderServlet") ? object.getString("renderServlet") : null; + + } + + public JsonObject getJsonObject(){ + return object; + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java new file mode 100644 index 0000000000..736d7fb9b6 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java @@ -0,0 +1,60 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import com.adobe.acs.commons.contentsync.impl.LastModifiedStrategy; +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.ResourceUtil; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; +import static org.apache.sling.jcr.resource.api.JcrResourceConstants.NT_SLING_FOLDER; + +public class ConfigurationUtils { + public static final String CONFIG_PATH = "/var/acs-commons/contentsync"; + public static final String SETTINGS_PATH = CONFIG_PATH + "/settings"; + public static final String HOSTS_PATH = CONFIG_PATH + "/hosts"; + + public static final String UPDATE_STRATEGY_KEY = "update-strategy"; + public static final String EVENT_USER_DATA_KEY = "event-user-data"; + + private ConfigurationUtils(){ + + } + + public static Resource getSettingsResource(ResourceResolver resourceResolver) throws PersistenceException { + Map resourceProperties = new HashMap<>(); + resourceProperties.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED); + resourceProperties.put(UPDATE_STRATEGY_KEY, LastModifiedStrategy.class.getName()); + resourceProperties.put(EVENT_USER_DATA_KEY, "changedByPageManagerCopy"); + return ResourceUtil.getOrCreateResource(resourceResolver, SETTINGS_PATH, resourceProperties, NT_SLING_FOLDER, true); + } + + public static Resource getHostsResource(ResourceResolver resourceResolver) throws PersistenceException { + Map resourceProperties = new HashMap<>(); + resourceProperties.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED); + return ResourceUtil.getOrCreateResource(resourceResolver, HOSTS_PATH, resourceProperties, NT_SLING_FOLDER, true); + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java new file mode 100644 index 0000000000..da80dbecb1 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java @@ -0,0 +1,84 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +public class ContentCatalog { + + private RemoteInstance remoteInstance; + private final String catalogServlet; + + public ContentCatalog(RemoteInstance remoteInstance, String catalogServlet) { + this.remoteInstance = remoteInstance; + this.catalogServlet = catalogServlet; + } + + public URI getFetchURI(String path, String updateStrategy) throws URISyntaxException { + return remoteInstance.toURI(catalogServlet, "root", path, "strategy", updateStrategy); + } + + public List fetch(String path, String updateStrategy) throws IOException, URISyntaxException { + URI uri = getFetchURI(path, updateStrategy); + + String json = remoteInstance.getString(uri); + + JsonObject response; + try(JsonReader reader = Json.createReader(new StringReader(json))) { + response = reader.readObject(); + } + if (!response.containsKey("resources")) { + throw new IOException("Failed to fetch content catalog from " + uri + ", Response: " + json); + } + JsonArray catalog = response.getJsonArray("resources"); + + return catalog.stream() + .map(JsonValue::asJsonObject) + .map(CatalogItem::new) + .collect(Collectors.toList()); + } + + public List getDelta(List catalog, ResourceResolver resourceResolver, UpdateStrategy updateStrategy) { + List lst = new ArrayList<>(); + for(CatalogItem item : catalog){ + Resource resource = resourceResolver.getResource(item.getPath()); + if(resource == null || updateStrategy.isModified(item, resource)){ + lst.add(item); + } + } + return lst; + } + +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java new file mode 100644 index 0000000000..9db0ee9134 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java @@ -0,0 +1,214 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +public class ContentReader { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + static final String BINARY_DATA_PLACEHOLDER = "0"; + + private final NodeTypeManager nodeTypeManager; + private final Collection knownPropertyPrefixes; + + public ContentReader(Session session) throws RepositoryException { + Workspace workspace = session.getWorkspace(); + + knownPropertyPrefixes = new HashSet<>(Arrays.asList(workspace.getNamespaceRegistry().getPrefixes())); + nodeTypeManager = workspace.getNodeTypeManager(); + } + + /** + * Recursive sanitize the give JCR node and remove protected properties + * + * @param node json node representing a JCR node + * @return sanitized json + * @see #getProtectedProperties(JsonObject) + */ + public JsonObject sanitize(JsonObject node) throws RepositoryException { + JsonObjectBuilder out = Json.createObjectBuilder(); + sanitize(node, out); + return out.build(); + } + + private void sanitize(JsonObject node, JsonObjectBuilder out) throws RepositoryException { + Collection sanitizedProperties = getProtectedProperties(node); + + for (Map.Entry field : node.entrySet()) { + String name = field.getKey(); + int colonIdx = name.indexOf(':'); + if (colonIdx > 0) { + // sanitize unknown namespaces. These can come, for example, from asset metadata + String prefix = name.substring(0, colonIdx); + if (!knownPropertyPrefixes.contains(prefix)) { + log.trace("skipping protected property: {}", name); + continue; + } + } + // sanitize protected properties + if (sanitizedProperties.contains(name)) { + log.trace("skipping unknown namespace: {}", name); + continue; + } + + JsonValue value = field.getValue(); + switch (value.getValueType()) { + case OBJECT: + JsonObjectBuilder obj = Json.createObjectBuilder(); + sanitize((JsonObject) value, obj); + out.add(name, obj); + break; + case ARRAY: + JsonArray array = (JsonArray) value; + out.add(name, array); + break; + default: + if (colonIdx == 0) { + // Leading colon in Sling GET Servlet JSON designates binary data, e.g. :jcr:data + // Put the real property instead (without a leading colon) and set a dummy value + out.add(name.substring(1), BINARY_DATA_PLACEHOLDER); + } else { + out.add(name, value); + } + break; + } + } + + } + + /** + * Collect protected properties of a given JCR node (non-recursively). + * The list of protected properties consists of: + * - properties protected by node's primary type + * - properties protected by node's mixins + *

+ * For example, if a cq:Page node does not have any mixins applied this method would return + *

+     *      ["jcr:created", "jcr:createdBy"]
+     *  
+ *

+ * If cq:Page is versionable, i.e. has the "mix:versionable" mixin type, then this method would return + * properties protected by the primary type (cq:Page ) and the mixin (mix:versionable) and the list would be + *

+     *      ["jcr:created", "jcr:createdBy", "jcr:versionHistory", "jcr:baseVersion", "jcr:predecessors",
+     *      "jcr:mergeFailed", "jcr:activity", "jcr:configuration", "jcr:isCheckedOut", "jcr:uuid" ]
+     *  
+ * + * @param node json representing a JCR node + * @return the list of protected properties + */ + public List getProtectedProperties(JsonObject node) throws RepositoryException { + Collection ignored = new HashSet<>(Arrays.asList(JCR_PRIMARYTYPE, JCR_MIXINTYPES)); + + List props = new ArrayList<>(); + props.add("rep:policy"); // ACLs are not importable + + List checkTypes = new ArrayList<>(); + String primaryType = node.getString(JCR_PRIMARYTYPE); + checkTypes.add(primaryType); + JsonArray mixins = node.getJsonArray(JCR_MIXINTYPES); + if (mixins != null) { + for (JsonValue item : mixins) { + checkTypes.add(((JsonString) item).getString()); + } + } + for (String typeName : checkTypes) { + NodeType nodeType = nodeTypeManager.getNodeType(typeName); + for (PropertyDefinition definition : nodeType.getPropertyDefinitions()) { + if (definition.isProtected() && !ignored.contains(definition.getName())) { + props.add(definition.getName()); + } + } + } + + return props; + } + + private void collectBinaryProperties(JsonObject node, String parent, List binaryProperties) { + for (Map.Entry field : node.entrySet()) { + String name = field.getKey(); + JsonValue value = field.getValue(); + switch (value.getValueType()) { + case OBJECT: + collectBinaryProperties((JsonObject) value, parent + "/" + name, binaryProperties); + break; + case NUMBER: + // leading colon in Sling GET Servlet JSON and a numeric value designate binary data + if (name.startsWith(":")) { + String propPath = parent + "/" + name.substring(1); + binaryProperties.add(propPath); + } + break; + default: + break; + } + } + } + + /** + * Recursively collect binary properties from a given json node. + *

+ * For example, if node represents a cq:Page object with inline images, + * the output would look like + * + *

+     *  [
+     *    /jcr:content/image/file/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_480.png/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_60.png/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_300.png/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_48.png/jcr:content/jcr:data
+     *  ]
+     * 
+ * + * @param node json representing a JCR node + * @return list of property paths relative to the json root + */ + public List collectBinaryProperties(JsonObject node) { + List binaryProperties = new ArrayList<>(); + collectBinaryProperties(node, "", binaryProperties); + return binaryProperties; + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java new file mode 100644 index 0000000000..fdf711e04e --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java @@ -0,0 +1,314 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import com.adobe.granite.workflow.WorkflowException; +import com.adobe.granite.workflow.WorkflowSession; +import com.adobe.granite.workflow.exec.WorkflowData; +import com.adobe.granite.workflow.model.WorkflowModel; +import com.day.cq.dam.api.Asset; +import com.day.cq.dam.api.AssetManager; +import com.day.cq.wcm.api.Page; +import com.day.cq.wcm.api.PageManager; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.jcr.contentloader.ContentImporter; +import org.apache.sling.jcr.contentloader.ImportOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.VersionManager; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.lang.invoke.MethodHandles; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +public class ContentSync { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private RemoteInstance remoteInstance; + private ContentImporter importer; + private ResourceResolver resourceResolver; + + public ContentSync(RemoteInstance remoteInstance, ResourceResolver resourceResolver, ContentImporter importer) { + this.remoteInstance = remoteInstance; + this.resourceResolver = resourceResolver; + this.importer = importer; + } + + /** + * Ensure that the order of child nodes matches the order on the remote instance. + *

+ * The method makes an HTTP call to the remote instance to fetch the ordered list of child nodes + * and re-sorts the given node to match it. + * + * @param node the node to sort + * @return children after sort + */ + public List sort(Node node) throws RepositoryException, IOException, URISyntaxException { + List children = remoteInstance.listChildren(node.getPath()); + sort(node, children); + return children; + } + + /** + * Sort child nodes of a JCR node + * + * @param node the node to sort + * @param children the desired order of children + */ + public void sort(Node node, List children) throws RepositoryException { + if (!node.getPrimaryNodeType().hasOrderableChildNodes()) { + // node does not support orderable child nodes + return; + } + + Node prev = null; + for (int i = 0; i < children.size(); i++) { + String childName = children.get(children.size() - 1 - i); + if (!node.hasNode(childName)) { + continue; + } + Node n = node.getNode(childName); + if (prev != null) { + node.orderBefore(n.getName(), prev.getName()); + } + prev = n; + } + } + + /** + * Copy binary data from remote instance and update local resource. + * Performs an HTT call for each property path + * + * @param propertyPaths list of binary properties to update, e.g. + *

+     *                      [
+     *                         "/content/contentsync/jcr:content/image/file/jcr:content/jcr:data",
+     *                         "/content/contentsync/jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_48.png/jcr:content/jcr:data",
+     *                      ]
+     *                      
+ */ + public void copyBinaries(List propertyPaths) throws IOException, RepositoryException, URISyntaxException { + Session session = resourceResolver.adaptTo(Session.class); + for (String propertyPath : propertyPaths) { + try (InputStream ntData = remoteInstance.getStream(propertyPath)) { + Binary binary = session.getValueFactory().createBinary(ntData); + Property p = session.getProperty(propertyPath); + if (p.getType() == PropertyType.BINARY) { + p.setValue(binary); + } else { + Node propertyNode = p.getParent(); + String propertyName = p.getName(); + p.remove(); + propertyNode.setProperty(propertyName, binary); + } + } + } + } + + public ImportOptions getImportOptions() { + return new ImportOptions() { + + @Override + public boolean isCheckin() { + return false; + } + + @Override + public boolean isAutoCheckout() { + return true; + } + + @Override + public boolean isIgnoredImportProvider(String extension) { + return false; + } + + @Override + public boolean isOverwrite() { + // preserve the node, uuid and version history + return false; + } + + @Override + public boolean isPropertyOverwrite() { + return true; + } + }; + } + + /** + * Clear jcr:content and remove all properties except protected ones + * + * @param node the node to clear + */ + public void clearContent(Node node) throws RepositoryException { + if (!node.hasNode(JCR_CONTENT)) { + return; + } + Node jcrContent = node.getNode(JCR_CONTENT); + if (!jcrContent.isCheckedOut()) { + VersionManager versionManager = node.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(jcrContent.getPath()); + } + // remove children of jcr:content + for (NodeIterator iterator = jcrContent.getNodes(); iterator.hasNext(); ) { + Node n = iterator.nextNode(); + n.remove(); + } + // remove any non-protected properties + for (PropertyIterator iterator = jcrContent.getProperties(); iterator.hasNext(); ) { + Property p = iterator.nextProperty(); + if (!p.getDefinition().isProtected()) { + p.remove(); + } + } + } + + /** + * Ensure parent node exists before importing content. + * + * Parent can be null, for example, if a user is sync-ing /content/my-site/en/one + * and the /content/my-site tree does not exist on the local instance. + * + * In such a case the method would fetch the primary type of the parent node (/content/my-site/en) + * and use it as intermediate node type to ensure parent. + * + * @param path the path to ensure if the parent exists + * @return the parent node + */ + public Node ensureParent(String path) throws RepositoryException, IOException, URISyntaxException { + String parentPath = ResourceUtil.getParent(path); + Session session = resourceResolver.adaptTo(Session.class); + Node parentNode; + if (!session.nodeExists(parentPath)) { + String parentNodeType = remoteInstance.getPrimaryType(parentPath); + parentNode = JcrUtils.getOrCreateByPath(parentPath, parentNodeType, parentNodeType, session, false); + } else { + parentNode = session.getNode(parentPath); + } + return parentNode; + } + + /** + * + * importContent("/content/contentsync/page", "jcr:content.json", .... ) + * where /content/contentsync/page is an existing cq:Page resource + * + * importContent("/content/dam/contentsync/asset", "jcr:content.json", .... ) + * where /content/contentsync/asset is an existing dam:Asset resource + * + * importContent("/content/dam/contentsync", "folderName.json", .... ) + * importContent("/content/misc", "nodeName.json", .... ) + * + * @param catalogItem + * @return + */ + public Node ensureContentNode(CatalogItem catalogItem) throws RepositoryException, IOException, URISyntaxException { + String path = catalogItem.getPath(); + + Node parentNode = ensureParent(path); + + Node contentNode; + if (catalogItem.hasContentResource()) { + String nodeName = ResourceUtil.getName(path); + + String primaryType = catalogItem.getString(JCR_PRIMARYTYPE); + if(parentNode.hasNode(nodeName)){ + contentNode = parentNode.getNode(nodeName); + } else { + contentNode = parentNode.addNode(nodeName, primaryType); + } + } else { + contentNode = parentNode; + } + return contentNode; + } + + public void importData(CatalogItem catalogItem, JsonObject jsonObject) throws RepositoryException, IOException, URISyntaxException { + String path = catalogItem.getPath(); + log.debug("importing {}", path); + + Node contentNode = ensureContentNode(catalogItem); + clearContent(contentNode); + + ImportOptions importOptions = getImportOptions(); + String nodeName; + if (catalogItem.hasContentResource()) { + nodeName = JCR_CONTENT; + } else { + nodeName = ResourceUtil.getName(path); + } + + StringWriter sw = new StringWriter(); + try(JsonWriter writer = Json.createWriter(sw)){ + writer.write(jsonObject); + } + InputStream contentStream = new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8)); + importer.importContent(contentNode, nodeName + ".json", contentStream, importOptions, null); + } + + @SuppressWarnings("squid:S112") + public String createVersion(Resource resource) throws Exception { + String revisionId = null; + if (resource.isResourceType("cq:Page")) { + PageManager pageManager = resourceResolver.adaptTo(PageManager.class); + Page pg = resource.adaptTo(Page.class); + if (pg != null) { + revisionId = pageManager.createRevision(pg, null, "created by contentsync").getId(); + } + } else if (resource.isResourceType("dam:Asset")) { + AssetManager assetManager = resourceResolver.adaptTo(AssetManager.class); + Asset asset = resource.adaptTo(Asset.class); + revisionId = assetManager.createRevision(asset, null, "created by contentsync").getId(); + } + return revisionId; + } + + public void runWorkflows(String workflowModel, List paths) throws WorkflowException { + WorkflowSession workflowSession = resourceResolver.adaptTo(WorkflowSession.class); + WorkflowModel model = workflowSession.getModel(workflowModel); + for (String path : paths) { + WorkflowData data = workflowSession.newWorkflowData("JCR_PATH", path); + workflowSession.startWorkflow(model, data); + } + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java new file mode 100644 index 0000000000..9b364e7e45 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java @@ -0,0 +1,176 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static javax.jcr.Property.JCR_PRIMARY_TYPE; + +/** + * HTTP connection to a remote AEM instance + some sugar methods to fetch data + */ +public class RemoteInstance implements Closeable { + private static final int CONNECT_TIMEOUT = 5000; + private static final int SOCKET_TIMEOUT = 60000; + + private final CloseableHttpClient httpClient; + private final SyncHostConfiguration hostConfiguration; + + public RemoteInstance(SyncHostConfiguration hostConfiguration) { + this.hostConfiguration = hostConfiguration; + this.httpClient = createHttpClient(); + } + + private CloseableHttpClient createHttpClient() { + BasicCredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials(hostConfiguration.getUsername(), hostConfiguration.getPassword())); + RequestConfig requestConfig = RequestConfig + .custom() + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT) + .setCookieSpec(CookieSpecs.STANDARD).build(); + return + HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setDefaultCredentialsProvider(provider) + .build(); + } + + public InputStream getStream(String path) throws IOException, URISyntaxException { + URI uri = toURI(path); + + return getStream(uri); + } + + public InputStream getStream(URI uri ) throws IOException { + HttpGet request = new HttpGet(uri); + CloseableHttpResponse response = httpClient.execute(request); + String msg; + switch (response.getStatusLine().getStatusCode()){ + case HttpStatus.SC_OK: + return response.getEntity().getContent(); + case HttpStatus.SC_MULTIPLE_CHOICES: + msg = formatError(uri.toString(), response.getStatusLine().getStatusCode(), + "It seems that the \"Json Max Results\" in Sling Get Servlet is too low. Increase it to a higher value, e.g. 1000."); + throw new IOException(msg); + default: + msg = formatError(uri.toString(), response.getStatusLine().getStatusCode(), "Response: " + EntityUtils.toString(response.getEntity())); + throw new IOException(msg); + } + } + + public String getString(URI uri) throws IOException { + HttpGet request = new HttpGet(uri); + try (CloseableHttpResponse response = httpClient.execute(request)) { + String str = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + return str; + } else { + String msg = formatError(uri.toString(), response.getStatusLine().getStatusCode(), "Response: " + str); + throw new IOException(msg); + } + } + } + + private String formatError(String uri, int statusCode, String message) { + return String.format("Failed to fetch data from %s, HTTP [%d]%n%s", uri, statusCode, message); + } + + public String getPrimaryType(String path) throws IOException, URISyntaxException { + URI uri = toURI(path + "/" + JCR_PRIMARY_TYPE); + String str = getString(uri); + if (str.isEmpty()) { + throw new IllegalStateException("It appears " + hostConfiguration.getUsername() + + " user does not have permissions to read " + uri); + } + return str; + } + + public List listChildren(String path) throws IOException, URISyntaxException { + List children; + try (InputStream is = getStream(path + ".1.json"); JsonReader reader = Json.createReader(is)) { + children = reader + .readObject() + .entrySet() + .stream() + .filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + return children; + } + + public JsonObject getJson(String path, String... parameters) throws IOException, URISyntaxException { + URI uri = toURI(path, parameters); + return getJson(uri); + } + + public JsonObject getJson(URI uri) throws IOException { + try (InputStream is = getStream(uri); JsonReader reader = Json.createReader(is)) { + return reader.readObject(); + } + } + + URI toURI(String path, String ... parameters) throws URISyntaxException { + URIBuilder ub = new URIBuilder(hostConfiguration.getHost()) + .setPath(path); + if(parameters != null) { + if (parameters.length % 2 != 0) { + throw new IllegalArgumentException("query string parameters must be an even number of name/values:" + Arrays.asList(parameters)); + } + for (int i = 0; i < parameters.length; i += 2) { + ub.addParameter(parameters[i], parameters[i + 1]); + } + } + return ub.build(); + } + + @Override + public void close() throws IOException { + httpClient.close(); + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java new file mode 100644 index 0000000000..6b01f7b5a6 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java @@ -0,0 +1,54 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.DefaultInjectionStrategy; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; +import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; + +/** + * Adapts to configuration nodes in /var/acs-commons/contentsync/hosts/* + */ +@Model(adaptables = {Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) +public class SyncHostConfiguration { + + @ValueMapValue(injectionStrategy = InjectionStrategy.REQUIRED) + private String host; + + @ValueMapValue + private String username; + + @ValueMapValue + private String password; + + public String getHost() { + return host; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java new file mode 100644 index 0000000000..7b9ce36bb5 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java @@ -0,0 +1,54 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; + +import java.util.List; + +public interface UpdateStrategy { + + /** + * This method is called on the remote instance when Content Sync requests the list of resources to sync + * + * @param request the request from the Content Sync UI + * @return the list of resources to sync + */ + List getItems(SlingHttpServletRequest request); + + + /** + * Compare local and remote resources and decided whether the resource was modified and need to be sync-ed + * + * @param remoteResource json representation of a remote resource + * @param localResource local resource + * @return whether the resource was modified + */ + boolean isModified(CatalogItem remoteResource, Resource localResource); + + /** + * + * @param remoteResource json representation of a remote resource + * @param localResource local resource + * @return message to print in the UI + */ + String getMessage(CatalogItem remoteResource, Resource localResource); +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java new file mode 100644 index 0000000000..1ae1a55319 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java @@ -0,0 +1,123 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync.impl; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import com.day.cq.dam.api.Asset; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.osgi.service.component.annotations.Component; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +@Component +public class AssetChecksumStrategy implements UpdateStrategy { + private static final String DAM_SHA1 = "dam:sha1"; + + @Override + public boolean isModified(CatalogItem remoteResource, Resource localResource) { + String remoteChecksum = getChecksum(remoteResource); + String localChecksum = getChecksum(localResource); + + return remoteChecksum != null && !remoteChecksum.equals(localChecksum); + } + + @Override + public List getItems(SlingHttpServletRequest request) { + String rootPath = request.getParameter("root"); + if (rootPath == null) { + throw new IllegalArgumentException("root request parameter is required"); + } + String query = "SELECT * FROM [dam:Asset] AS s WHERE ISDESCENDANTNODE([" + rootPath + "]) ORDER BY s.[jcr:path]"; + Iterator it = request.getResourceResolver().findResources(query, "JCR-SQL2"); + List items = new ArrayList<>(); + while (it.hasNext()) { + Resource res = it.next(); + Asset asset = res.adaptTo(Asset.class); + if(asset == null){ + continue; + } + JsonObjectBuilder json = Json.createObjectBuilder(); + writeMetadata(json, res); + items.add(new CatalogItem(json.build())); + } + return items; + } + + @Override + @SuppressWarnings("squid:S2583") + public String getMessage(CatalogItem remoteResource, Resource localResource) { + String remoteChecksum = getChecksum(remoteResource); + String localChecksum = getChecksum(localResource); + + boolean modified = remoteChecksum != null && !remoteChecksum.equals(localChecksum); + StringBuilder msg = new StringBuilder(); + if (localResource == null) { + msg.append("resource does not exist"); + } else { + msg.append(modified ? "resource modified ... " : "replacing ... "); + if (localChecksum != null) { + msg.append('\n'); + msg.append("\tlocal checksum: " + localChecksum); + } + if (remoteChecksum != null) { + msg.append('\n'); + msg.append("\tremote checksum: " + remoteChecksum); + } + } + return msg.toString(); + } + + private String getChecksum(CatalogItem remoteItem) { + return remoteItem.getString(DAM_SHA1); + } + + @SuppressWarnings("squid:S1144") + private String getChecksum(Resource targetResource) { + if (targetResource == null) { + return null; + } + Asset asset = targetResource.adaptTo(Asset.class); + return asset == null ? null : (String) asset.getMetadata(DAM_SHA1); + } + + public void writeMetadata(JsonObjectBuilder jw, Resource res) { + jw.add("path", res.getPath()); + jw.add(JCR_PRIMARYTYPE, res.getValueMap().get(JCR_PRIMARYTYPE, String.class)); + + Resource jcrContent = res.getChild(JCR_CONTENT); + String exportUri; + if (jcrContent != null) { + exportUri = jcrContent.getPath() + ".infinity.json"; + } else { + exportUri = res.getPath() + ".json"; + } + jw.add("exportUri", exportUri); + jw.add(DAM_SHA1, getChecksum(res)); + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java new file mode 100644 index 0000000000..24123a5566 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java @@ -0,0 +1,264 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync.impl; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import com.adobe.granite.security.user.util.AuthorizableUtil; +import com.day.cq.commons.PathInfo; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestPathInfo; +import org.apache.sling.api.resource.AbstractResourceVisitor; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.api.servlets.ServletResolver; +import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.servlet.GenericServlet; +import javax.servlet.Servlet; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +@Component +public class LastModifiedStrategy implements UpdateStrategy { + public static final String DEFAULT_GET_SERVLET = "org.apache.sling.servlets.get.DefaultGetServlet"; + public static final String REDIRECT_SERVLET = "org.apache.sling.servlets.get.impl.RedirectServlet"; + + @Reference + private ServletResolver servletResolver; + + @Override + public List getItems(SlingHttpServletRequest request) { + String rootPath = request.getParameter("root"); + if (rootPath == null) { + throw new IllegalArgumentException("root request parameter is required"); + } + + Resource root = request.getResourceResolver().getResource(rootPath); + if (root == null) { + return Collections.emptyList(); + } + + List items = new ArrayList<>(); + new AbstractResourceVisitor() { + @Override + public void visit(Resource res) { + if (!accepts(res)) { + return; + } + JsonObjectBuilder json = Json.createObjectBuilder(); + writeMetadata(json, res, request); + items.add(new CatalogItem(json.build())); + } + }.accept(root); + return items; + } + + @Override + public boolean isModified(CatalogItem remoteResource, Resource localResource) { + LastModifiedInfo remoteLastModified = getLastModified(remoteResource); + LastModifiedInfo localLastModified = getLastModified(localResource); + + return remoteLastModified.getLastModified() > localLastModified.getLastModified(); + } + + @Override + @SuppressWarnings("squid:S2583") + public String getMessage(CatalogItem remoteResource, Resource localResource) { + LastModifiedInfo remoteLastModified = getLastModified(remoteResource); + LastModifiedInfo localLastModified = getLastModified(localResource); + + boolean modified = remoteLastModified.getLastModified() > localLastModified.getLastModified(); + SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy, h:mm:ss a"); + StringBuilder msg = new StringBuilder(); + if (localResource == null) { + msg.append("resource does not exist"); + } else { + msg.append(modified ? "resource modified ... " : "replacing ... "); + if (localLastModified.getLastModified() > 0) { + msg.append('\n'); + msg.append("\tlocal lastModified: " + dateFormat.format(localLastModified.getLastModified()) + " by " + localLastModified.getLastModifiedBy()); + } + if (remoteLastModified.getLastModified() > 0) { + msg.append('\n'); + msg.append("\tremote lastModified: " + dateFormat.format(remoteLastModified.getLastModified()) + " by " + remoteLastModified.getLastModifiedBy()); + } + } + return msg.toString(); + } + + /** + * Determines whether to write the resource in the catalog json. + *

+ * For example, implementations can return only dam:Asset nodes, + * or any nt:unstructured nodes, etc. + * + * @param resource the resource to check + * @return whether to write the resource in the catalog + */ + boolean accepts(Resource resource) { + if ( + // don't drill down into jcr:content. The entire content will be grabbed by jcr:content.infinity.json + resource.getPath().contains("/" + JCR_CONTENT) + // ignore rep:policy, rep:cugPolicy, rep:restrictions and such + || resource.getPath().contains("/rep:") + ) { + return false; + } + return true; + } + + /** + * Returns the render servlet for the given urlPath . + * + * @param urlPath the json export url,. e.g. /content/wknd/page/jcr:content.infinity.json + * @return the render servlet, e.g. org.apache.sling.servlets.get.DefaultGetServlet + */ + String getJsonRendererServlet(SlingHttpServletRequest slingRequest, String urlPath) { + Resource resource = slingRequest.getResourceResolver().resolve(urlPath); + Servlet s = servletResolver.resolveServlet(new SlingHttpServletRequestWrapper(slingRequest) { + @Override + public Resource getResource() { + return resource; + } + + @Override + public String getMethod() { + return "GET"; + } + + @Override + public RequestPathInfo getRequestPathInfo() { + return new PathInfo(urlPath); + } + }); + String servletName = null; + if (s instanceof GenericServlet) { + GenericServlet genericServlet = (GenericServlet) s; + servletName = genericServlet.getServletName(); + } + // Sling Redirect Servlet handles json exports by forwarding to DefaultGetServlet. So do we. + if (REDIRECT_SERVLET.equals(servletName)) { + servletName = DEFAULT_GET_SERVLET; + } + return servletName; + } + + void writeMetadata(JsonObjectBuilder jw, Resource res, SlingHttpServletRequest request) { + jw.add("path", res.getPath()); + jw.add(JCR_PRIMARYTYPE, res.getValueMap().get(JCR_PRIMARYTYPE, String.class)); + + Resource jcrContent = res.getChild(JCR_CONTENT); + String exportUri; + Resource contentResource; + if (jcrContent != null) { + contentResource = jcrContent; + exportUri = jcrContent.getPath() + ".infinity.json"; + } else { + contentResource = res; + exportUri = res.getPath() + ".json"; + } + + String renderServlet = getJsonRendererServlet(request, exportUri); + // check if the resource is rendered by DefaultGetServlet, i.e. is exportable + // if it isn't, go one level up and try the parent + if (!DEFAULT_GET_SERVLET.equals(renderServlet)) { + contentResource = contentResource.getParent(); + exportUri = contentResource.getPath() + ".infinity.json"; + } + + // last try: if the rendering servlet is still not DefaultGetServlet then + // put a flag in the output. + renderServlet = getJsonRendererServlet(request, exportUri); + if (!DEFAULT_GET_SERVLET.equals(renderServlet)) { + jw.add("renderServlet", renderServlet); + } + jw.add("exportUri", exportUri); + + LastModifiedInfo lastModified = getLastModified(res); + + if (lastModified.getLastModified() > 0L) { + jw.add("lastModified", lastModified.getLastModified()); + } + if (lastModified.getLastModifiedBy() != null) { + jw.add("lastModifiedBy", lastModified.getLastModifiedBy()); + } + } + + private LastModifiedInfo getLastModified(CatalogItem item) { + long lastModified = item.getLong("lastModified"); + String lastModifiedBy = item.getString("lastModifiedBy"); + return new LastModifiedInfo(lastModified, lastModifiedBy); + } + + @SuppressWarnings("squid:S1144") + private LastModifiedInfo getLastModified(Resource targetResource) { + long lastModified = 0L; + String lastModifiedBy = null; + if (targetResource != null) { + Resource contentResource = targetResource.getChild(JCR_CONTENT); + if (contentResource == null) { + contentResource = targetResource; + } + ValueMap vm = contentResource.getValueMap(); + Calendar c = (Calendar) vm.get("cq:lastModified", (Class) Calendar.class); + if (c == null) { + c = (Calendar) vm.get("jcr:lastModified", (Class) Calendar.class); + } + if (c != null) { + lastModified = c.getTime().getTime(); + } + String modifiedBy = (String) vm.get("cq:lastModifiedBy", (Class) String.class); + if (modifiedBy == null) { + modifiedBy = (String) vm.get("jcr:lastModifiedBy", (Class) String.class); + } + lastModifiedBy = AuthorizableUtil.getFormattedName(targetResource.getResourceResolver(), modifiedBy); + } + return new LastModifiedInfo(lastModified, lastModifiedBy); + } + + private static class LastModifiedInfo { + private final long lastModified; + private final String lastModifiedBy; + + public LastModifiedInfo(long lastModified, String lastModifiedBy) { + this.lastModified = lastModified; + this.lastModifiedBy = lastModifiedBy; + } + + public long getLastModified() { + return lastModified; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java new file mode 100644 index 0000000000..7b3da34cc6 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java @@ -0,0 +1,21 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package com.adobe.acs.commons.contentsync; diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java new file mode 100644 index 0000000000..1ee787c13b --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java @@ -0,0 +1,109 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync.servlet; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObjectBuilder; +import javax.json.JsonWriter; +import javax.servlet.Servlet; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + +@Component(service = Servlet.class, immediate = true, property = { + "sling.servlet.extensions=json", + "sling.servlet.selectors=catalog", + "sling.servlet.resourceTypes=acs-commons/components/utilities/contentsync", +}) +public class ContentCatalogServlet extends SlingSafeMethodsServlet { + + private final transient Map updateStrategies = Collections.synchronizedMap(new LinkedHashMap<>()); + + @Reference(service = UpdateStrategy.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC) + protected void bindDeltaStrategy(UpdateStrategy strategy) { + if (strategy != null) { + String key = strategy.getClass().getName(); + updateStrategies.put(key, strategy); + } + } + + protected void unbindDeltaStrategy(UpdateStrategy strategy) { + String key = strategy.getClass().getName(); + updateStrategies.remove(key); + } + + @Override + protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { + response.setContentType("application/json"); + + JsonObjectBuilder result = Json.createObjectBuilder(); + try { + JsonArrayBuilder resources = Json.createArrayBuilder(); + String pid = request.getParameter("strategy"); + UpdateStrategy updateStrategy = getStrategy(pid); + List items = updateStrategy.getItems(request); + + for (CatalogItem item : items) { + resources.add(item.getJsonObject()); + } + result.add("resources", resources); + } catch (Exception e){ + result.add("error", e.getMessage()); + response.setStatus(SC_INTERNAL_SERVER_ERROR); + } + + try(JsonWriter out = Json.createWriter(response.getWriter())){ + out.writeObject(result.build()); + } + } + + UpdateStrategy getStrategy(String pid) { + UpdateStrategy strategy; + if(pid == null){ + strategy = updateStrategies.values().iterator().next(); + } else { + strategy = updateStrategies.get(pid); + if(strategy == null){ + throw new IllegalArgumentException("Cannot find UpdateStrategy for pid " + pid + "." + + " Available strategies: " + updateStrategies.values() + .stream().map(s -> s.getClass().getName()).collect(Collectors.toList())); + } + } + return strategy; + } +} \ No newline at end of file diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java new file mode 100644 index 0000000000..bb9ae7d2fb --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java @@ -0,0 +1,221 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.json.Json; +import javax.json.JsonObject; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestContentReader { + @Rule + public AemContext context = new AemContext(ResourceResolverType.JCR_OAK); + + ContentReader reader; + JsonObject jcrContent; + + @Before + public void setUp() throws RepositoryException { + reader = new ContentReader(context.resourceResolver().adaptTo(Session.class)); + jcrContent = Json.createReader(getClass().getResourceAsStream("/contentsync/jcr_content.json")).readObject(); + } + + @Test + public void collectBinaryData() { + + List props = reader.collectBinaryProperties(jcrContent); + List expected = Arrays.asList( + "/customBinaryProperty", + "/image/file/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_480.png/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_60.png/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_300.png/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_48.png/jcr:content/jcr:data" + ); + assertEquals(expected, props); + } + + @Test + public void collectProtectedProperties() throws Exception { + + JsonObject page1 = Json.createObjectBuilder() + .add("jcr:primaryType", "cq:PageContent") + .build(); + assertEquals(Arrays.asList("rep:policy", "jcr:created", "jcr:createdBy"), reader.getProtectedProperties(page1)); + + JsonObject page2 = Json.createObjectBuilder() + .add("jcr:primaryType", "cq:PageContent") + .add("jcr:mixinTypes", Json.createArrayBuilder().add("mix:versionable").build()) + .build(); + + assertEquals( + Arrays.asList( + "rep:policy", "jcr:created", "jcr:createdBy", "jcr:versionHistory", "jcr:baseVersion", "jcr:predecessors", + "jcr:mergeFailed", "jcr:activity", "jcr:configuration", "jcr:isCheckedOut", "jcr:uuid"), + reader.getProtectedProperties(page2)); + } + + @Test + public void sanitizeProtectedProperties() throws Exception { + JsonObject node = Json.createObjectBuilder() + .add("jcr:primaryType", "cq:PageContent") + .add("jcr:mixinTypes", Json.createArrayBuilder().add("mix:versionable").add("rep:AccessControllable").build()) + .add("jcr:created", "sanitize me") + .add("jcr:createdBy", "sanitize me") + .add("jcr:uuid", "sanitize me") + .add("jcr:versionHistory", "sanitize me") + .add("jcr:predecessors", "sanitize me") + .add("referenceable", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("jcr:mixinTypes", Json.createArrayBuilder().add("mix:referenceable").build()) + .add("jcr:uuid", "sanitize me") + .add("jcr:created", "retain me") + .add("jcr:createdBy", "retain me") + .build()) + .add("content", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("jcr:created", "retain me") + .add("jcr:createdBy", "retain me") + .add("jcr:uuid", "retain me") + .build()) + .add("rep:policy", Json.createObjectBuilder() + .add("jcr:primaryType", "rep:ACL") + .add("allow", Json.createObjectBuilder() + .add("jcr:primaryType", "rep:GrantACE") + .add("rep:principalName", "everyone") + .add("rep:privileges", Json.createArrayBuilder().add("jcr:read").build()) + .build()) + .build()) + .build(); + + JsonObject sanitizedContent = reader.sanitize(node); + + assertFalse(sanitizedContent.containsKey("jcr:created")); // protected by mix:created via cq:PageContent + assertFalse(sanitizedContent.containsKey("jcr:createdBy")); // protected by mix:created via cq:PageContent + assertFalse(sanitizedContent.containsKey("jcr:uuid")); // protected by mix:referenceable via mix:versionable + assertFalse(sanitizedContent.containsKey("jcr:versionHistory")); // protected by mix:referenceable via mix:versionable + assertFalse(sanitizedContent.containsKey("jcr:predecessors")); // protected by mix:referenceable via mix:versionable + + JsonObject referenceable = sanitizedContent.getJsonObject("referenceable"); + assertFalse(referenceable.containsKey("jcr:uuid")); + // jcr:created and jcr:createdBy are retained because the node is not mix:created and these two are not protected + assertTrue(referenceable.containsKey("jcr:created")); + assertTrue(referenceable.containsKey("jcr:createdBy")); + + JsonObject content = sanitizedContent.getJsonObject("content"); + // a plain nt:unstructured node can have any property + assertTrue(content.containsKey("jcr:uuid")); + assertTrue(content.containsKey("jcr:created")); + assertTrue(content.containsKey("jcr:createdBy")); + + // ACls are not importable + assertFalse(sanitizedContent.containsKey("rep:policy")); + } + + @Test + public void sanitizeUnknownNamespaces() throws Exception { + JsonObject content = Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("test1:one", "sanitize me") + .add("test1:two", "sanitize me") + .add("metadata", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("test2:one", "sanitize me") + .add("test2:two", "sanitize me") + .build()) + .add("test3:one", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .build()) + .build(); + + JsonObject sanitizedContent = reader.sanitize(content); + assertFalse(sanitizedContent.containsKey("test1:one")); + assertFalse(sanitizedContent.containsKey("test1:two")); + + assertFalse(sanitizedContent.containsKey("test3:one")); + + JsonObject metadata = sanitizedContent.getJsonObject("metadata"); + assertFalse(metadata.containsKey("test2:one")); + assertFalse(metadata.containsKey("test2:two")); + } + + @Test + public void sanitizeBinaryProperties() throws Exception { + JsonObject node = Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add(":jcr:data", 100L) + .add(":customProperty", 200L) + .add("file", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:file") + .add("jcr:content", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:resource") + .add(":jcr:data", 300L) + .build()) + .build()) + .build(); + + JsonObject sanitizedContent = reader.sanitize(node); + + assertFalse(sanitizedContent.containsKey(":jcr:data")); + assertEquals(ContentReader.BINARY_DATA_PLACEHOLDER, sanitizedContent.getString("jcr:data")); + + assertFalse(sanitizedContent.containsKey(":customProperty")); + assertEquals(ContentReader.BINARY_DATA_PLACEHOLDER, sanitizedContent.getString("customProperty")); + + JsonObject file = sanitizedContent.getValue("/file/jcr:content").asJsonObject(); + assertFalse(file.containsKey(":jcr:data")); + assertEquals(ContentReader.BINARY_DATA_PLACEHOLDER, file.getString("jcr:data")); + } + + /** + * A node type exists on the source instance, but not on the target. + * + * In this case the import fails with a NoSuchNodeTypeException. + */ + @Test(expected = NoSuchNodeTypeException.class) + public void sanitizeUnknownPropertyTypes() throws Exception { + JsonObject node = Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("node1", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:aaa") + .build()) + .add("node2", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:bbb") + .build()) + .build(); + + + JsonObject sanitizedContent = reader.sanitize(node); + + } +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java new file mode 100644 index 0000000000..1d4f810896 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java @@ -0,0 +1,408 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync; + +import com.day.cq.dam.api.Asset; +import com.day.cq.wcm.api.Page; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.commons.io.IOUtils; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.jcr.contentloader.ContentImporter; +import org.apache.sling.jcr.contentloader.internal.ContentReaderWhiteboard; +import org.apache.sling.jcr.contentloader.internal.DefaultContentImporter; +import org.apache.sling.jcr.contentloader.internal.readers.JsonReader; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.json.Json; +import javax.json.JsonObject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class TestContentSync { + @Rule + public AemContext context = new AemContext(ResourceResolverType.JCR_OAK); + + ContentSync contentSync; + ContentReader reader; + RemoteInstance remoteInstance; + + @Before + public void setUp() throws RepositoryException { + context.registerInjectActivateService(new JsonReader()); + context.registerInjectActivateService(new ContentReaderWhiteboard()); + context.addModelsForClasses(SyncHostConfiguration.class); + reader = new ContentReader(context.resourceResolver().adaptTo(Session.class)); + + String configPath = "/var/acs-commons/contentsync/config"; + context.build().resource(configPath, "host", "http://localhost:4502", "username", "", "password", ""); + SyncHostConfiguration hostConfiguration = context.resourceResolver().getResource(configPath).adaptTo(SyncHostConfiguration.class); + ContentImporter contentImporter = context.registerInjectActivateService(new DefaultContentImporter()); + remoteInstance = spy(new RemoteInstance(hostConfiguration)); + + contentSync = new ContentSync(remoteInstance, context.resourceResolver(), contentImporter); + } + + @Test + public void sortNodes() throws Exception { + context.build().atParent() + .resource("/content/sorted", "jcr:primaryType", "cq:Page") + .resource("/content/sorted/one") + .resource("/content/sorted/two") + .resource("/content/sorted/three") + ; + Node node = context.resourceResolver().getResource("/content/sorted").adaptTo(Node.class); + // assert initial ordering + assertEquals(Arrays.asList("one", "two", "three"), getChildNodeNames(node)); + + contentSync.sort(node, Arrays.asList("three", "one", "two")); + assertEquals(Arrays.asList("three", "one", "two"), getChildNodeNames(node)); + + // assert initial ordering + contentSync.sort(node, Arrays.asList("one", "unknown", "three", "rep:policy", "two")); // unknown / non-synced nodes are ignored + assertEquals(Arrays.asList("one", "three", "two"), getChildNodeNames(node)); + + context.build() + .resource("/content/nonsortable", "jcr:primaryType", "sling:Folder") + .resource("/content/nonsortable/three") + .resource("/content/nonsortable/two") + .resource("/content/nonsortable/one") + ; + + // The sort operation has no effect if the parent node does not support orderable child nodes + node = context.resourceResolver().getResource("/content/nonsortable").adaptTo(Node.class); + assertEquals(Arrays.asList("three", "two", "one"), getChildNodeNames(node)); + contentSync.sort(node, Arrays.asList("one", "three", "rep:policy", "two")); + assertEquals(Arrays.asList("three", "two", "one"), getChildNodeNames(node)); // ordering didn't change + } + + /** + * @return list of names of child nodes + */ + private List getChildNodeNames(Node node) throws RepositoryException { + List children = new ArrayList<>(); + for (NodeIterator it = node.getNodes(); it.hasNext(); ) { + Node child = it.nextNode(); + children.add(child.getName()); + } + return children; + } + + @Test + public void sortNodesFromRemote() throws Exception { + String contentRoot = "/content/dam/riverside-camping-australia"; + context.load().json(getClass().getResourceAsStream("/contentsync/riverside-camping-australia.1.json"), contentRoot); + + //fake HTTP call to get the list of children from, remote + doReturn(getClass().getResourceAsStream("/contentsync/riverside-camping-australia.1.json")) + .when(remoteInstance).getStream(anyString()); + + Node node = context.resourceResolver().getResource(contentRoot).adaptTo(Node.class); + List children = contentSync.sort(node); + + assertEquals(Arrays.asList( + "adobestock-216674449.jpeg", + "jcr:content", + "adobe_waadobe_wa_mg_2466.jpg", + "riverside-camping-australia", + "adobestock-257541512.jpeg", + "adobestock-238491803.jpeg", + "adobestock-178022573.jpeg", + "adobestock-167833331.jpeg"), children); + } + + @Test + public void testClearContent() throws Exception { + String path = "/content/contentsync/page"; + context.build().resource(path, "jcr:primaryType", "cq:Page"); + context.load().json(getClass().getResourceAsStream("/contentsync/jcr_content.json"), path + "/jcr:content"); + + Node node = context.resourceResolver().getResource(path).adaptTo(Node.class); + Node jcrContent = node.getNode("jcr:content"); + + List userProps = Arrays.asList("jcr:title", "cq:conf", "cq:designPath"); + List systemProps = Arrays.asList( + "jcr:created", "jcr:createdBy", "jcr:uuid", "jcr:versionHistory", + "jcr:predecessors", "jcr:isCheckedOut", "jcr:mixinTypes", "jcr:primaryType", "jcr:baseVersion"); + + + assertTrue(jcrContent.getNodes().hasNext()); // jcr:content has children initially + for (String name : userProps) { + assertTrue(name, jcrContent.hasProperty(name)); // there is user data in jcr:content + } + for (String name : systemProps) { + assertTrue(name, jcrContent.hasProperty(name)); // there are system properties + } + + contentSync.clearContent(node); + + assertFalse(jcrContent.getNodes().hasNext()); // no children under jcr:content + for (String name : userProps) { + assertFalse(name, jcrContent.hasProperty(name)); // user properties in jcr:content are cleared + } + for (String name : systemProps) { + assertTrue(name, jcrContent.hasProperty(name)); // system properties are retained + } + } + + @Test + public void testEnsureParent_NonExisting() throws IOException, RepositoryException, URISyntaxException { + String path = "/content/my-site/folder/page"; + + // parent does not exist. make HTTP call to fetch jcr:primaryType of it from remote + doReturn("cq:Page").when(remoteInstance).getPrimaryType(anyString()); + Node parent = contentSync.ensureParent(path); + + assertEquals("/content/my-site/folder", parent.getPath()); + assertEquals("cq:Page", parent.getPrimaryNodeType().getName()); + assertEquals("cq:Page", parent.getParent().getPrimaryNodeType().getName()); + } + + @Test + public void testEnsureParent_Existing() throws IOException, RepositoryException, URISyntaxException { + String path = "/content/my-site/folder/page"; + context.build().withIntermediatePrimaryType("sling:Folder") + .resource(path); + + Node parent = contentSync.ensureParent(path); + verify(remoteInstance, never()).getPrimaryType(anyString()); // no need to call the remote instance + + assertEquals("/content/my-site/folder", parent.getPath()); + assertEquals("sling:Folder", parent.getPrimaryNodeType().getName()); + assertEquals("sling:Folder", parent.getParent().getPrimaryNodeType().getName()); + } + + @Test + public void testCopyBinaryData_ExistingProperty() throws Exception { + context.build() + .resource("/content/image") + .file("file", new ByteArrayInputStream(new byte[]{1, 2, 3}), "text/plain", 0L); + + // fetch binary data from remote + doReturn(new ByteArrayInputStream(new byte[]{2, 3})).when(remoteInstance).getStream(anyString()); + + String propertyPath = "/content/image/file/jcr:content/jcr:data"; + List paths = Arrays.asList(propertyPath); + contentSync.copyBinaries(paths); + + byte[] data = IOUtils.toByteArray( + context.resourceResolver().adaptTo(Session.class).getProperty(propertyPath).getBinary().getStream() + ); + assertArrayEquals(new byte[]{2, 3}, data); + + } + + @Test + public void testCopyBinaryData_NewProperty() throws Exception { + context.build() + .resource("/content/image/file", "jcr:primaryType", "nt:file") + .resource("/content/image/file/jcr:content", + "jcr:primaryType", "nt:resource", "jcr:data", ContentReader.BINARY_DATA_PLACEHOLDER) + .commit() + ; + + // fetch binary data from remote + doReturn(new ByteArrayInputStream(new byte[]{2, 3})).when(remoteInstance).getStream(anyString()); + + String propertyPath = "/content/image/file/jcr:content/jcr:data"; + List paths = Arrays.asList(propertyPath); + contentSync.copyBinaries(paths); + + byte[] data = IOUtils.toByteArray( + context.resourceResolver().adaptTo(Session.class).getProperty(propertyPath).getBinary().getStream() + ); + assertArrayEquals(new byte[]{2, 3}, data); + } + + + @Test + public void testImportNewAsset() throws Exception { + context.build().resource("/content/dam", "jcr:primaryType", "sling:OrderedFolder"); + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", "/content/dam/asset") + .add("exportUri", "/content/dam/asset/jcr:content.infinity.json") + .add("jcr:primaryType", "dam:Asset") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/asset.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + Asset asset = context.resourceResolver().getResource("/content/dam/asset").adaptTo(Asset.class); + + byte[] data = IOUtils.toByteArray( + asset.getOriginal().getStream() + ); + assertArrayEquals(ContentReader.BINARY_DATA_PLACEHOLDER.getBytes(), data); + assertEquals("image/jpeg", asset.getMimeType()); + + assertEquals("no", asset.getMetadata("dam:Progressive")); + assertEquals("Adobe PDF library 15.00", asset.getMetadata("pdf:Producer")); + assertEquals((long) 657, asset.getMetadata("tiff:ImageWidth")); + } + + @Test + public void testUpdateExistingAsset() throws Exception { + context.build().resource("/content/dam", "jcr:primaryType", "sling:OrderedFolder"); + String assetPath = "/content/dam/asset"; + context.assetManager().createAsset(assetPath, new ByteArrayInputStream(new byte[]{1, 2, 3}), "text/plain", false); + context.resourceResolver() + .getResource(assetPath + "/jcr:content/metadata") + .adaptTo(ModifiableValueMap.class) + .put("test", "remove me"); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", assetPath) + .add("exportUri", assetPath + "/jcr:content.infinity.json") + .add("jcr:primaryType", "dam:Asset") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/asset.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + Asset asset = context.resourceResolver().getResource(assetPath).adaptTo(Asset.class); + + byte[] data = IOUtils.toByteArray( + asset.getOriginal().getStream() + ); + assertArrayEquals(ContentReader.BINARY_DATA_PLACEHOLDER.getBytes(), data); + assertEquals("image/jpeg", asset.getMimeType()); + + assertEquals(null, asset.getMetadata("test")); // any properties from that existed before import are wiped off + assertEquals("no", asset.getMetadata("dam:Progressive")); + assertEquals("Adobe PDF library 15.00", asset.getMetadata("pdf:Producer")); + assertEquals((long) 657, asset.getMetadata("tiff:ImageWidth")); + } + + @Test + public void testUpdatePagePreserveVersionHistory() throws Exception { + context.build().resource("/content/wknd", "jcr:primaryType", "cq:Page"); + Page page = context.pageManager().create("/content/wknd", "test", "test", "Test"); + Node jcrContent = page.getContentResource().adaptTo(Node.class); + + Session session = context.resourceResolver().adaptTo(Session.class); + VersionManager versionManager = session.getWorkspace().getVersionManager(); + + createVersion(jcrContent, "Version 1"); + + String pagePath = "/content/wknd/test"; + assertNotNull(versionManager.getVersionHistory(pagePath + "/jcr:content").getVersionByLabel("Version 1")); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("exportUri", pagePath + "/jcr:content.infinity.json") + .add("jcr:primaryType", "cq:Page") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/wknd-faqs.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + // assert the version is still there + assertNotNull(versionManager.getVersionHistory(pagePath + "/jcr:content").getVersionByLabel("Version 1")); + } + + /** + * Mimic PageManagerImpl#createVersion + */ + private void createVersion(Node node, String versionLabel) throws RepositoryException { + Session session = context.resourceResolver().adaptTo(Session.class); + VersionManager versionManager = session.getWorkspace().getVersionManager(); + node.addMixin("mix:versionable"); + session.save(); + + try { + Version v = versionManager.checkin(node.getPath()); + v.getContainingHistory().addVersionLabel(v.getName(), versionLabel, false); + + } finally { + versionManager.checkout(node.getPath()); + } + session.save(); + } + + @Test + public void testImportFolder() throws Exception { + context.build().resource("/content/wknd", "jcr:primaryType", "cq:Page"); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", "/content/wknd/test") + .add("exportUri", "/content/wknd/test.json") + .add("jcr:primaryType", "sling:OrderedFolder") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/ordered-folder.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + ValueMap vm = context.resourceResolver().getResource("/content/wknd/test").getValueMap(); + assertEquals("Wknd Fragments", vm.get("jcr:title")); + assertEquals("sling:OrderedFolder", vm.get("jcr:primaryType")); + assertEquals("html", vm.get("cq:adobeTargetExportFormat")); + } + + @Test + public void testAutoCheckout() throws Exception { + String path = "/content/wknd/page"; + Page pg = context.create().page(path); + Node jcrContent = pg.getContentResource().adaptTo(Node.class); + createVersion(jcrContent, "test 1"); + jcrContent.checkin(); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", path) + .add("exportUri", path + "/jcr:content.infinity.json") + .add("jcr:primaryType", "cq:Page") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/wknd-faqs.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + ValueMap vm = context.resourceResolver().getResource(path + "/jcr:content").getValueMap(); + assertEquals("FAQs", vm.get("jcr:title")); + assertEquals(true, vm.get("jcr:isCheckedOut")); + } +} \ No newline at end of file diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java new file mode 100644 index 0000000000..25c1aafb34 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java @@ -0,0 +1,202 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync.impl; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import com.day.cq.wcm.api.Page; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.servlets.ServletResolver; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.servlet.GenericServlet; +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.List; + +import static com.adobe.acs.commons.contentsync.impl.LastModifiedStrategy.DEFAULT_GET_SERVLET; +import static com.adobe.acs.commons.contentsync.impl.LastModifiedStrategy.REDIRECT_SERVLET; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class TestLastModifiedStrategy { + + @Rule + public AemContext context = new AemContext(ResourceResolverType.RESOURCEPROVIDER_MOCK); + + private UpdateStrategy updateStrategy; + + private ServletResolver servletResolver; + + @Before + public void setUp() { + servletResolver = mock(ServletResolver.class); + context.registerService(ServletResolver.class, servletResolver); + updateStrategy = context.registerInjectActivateService(new LastModifiedStrategy()); + } + + /** + * isModified() returns false if cq:lastModified/jcr:lastModified is not set + */ + @Test + public void testLastModifiedNA() { + String pagePath = "/content/wknd/page"; + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("jcr:primaryType", "cq:Page") + .build(); + + Page page = context.create().page(pagePath); + Resource pageResource = page.adaptTo(Resource.class); + assertFalse(updateStrategy.isModified(new CatalogItem(catalogItem), pageResource)); + } + + @Test + public void testPageModified() { + String pagePath = "/content/wknd/page"; + ZonedDateTime remoteTimestamp = ZonedDateTime.now().minusDays(1); + ZonedDateTime localTimestamp = ZonedDateTime.now().minusDays(2); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("jcr:primaryType", "cq:Page") + .add("lastModified", remoteTimestamp.toInstant().toEpochMilli()) + .build(); + + Page page = context.create().page(pagePath, null, Collections.singletonMap("cq:lastModified", GregorianCalendar.from(localTimestamp))); + Resource pageResource = page.adaptTo(Resource.class); + + assertTrue(updateStrategy.isModified(new CatalogItem(catalogItem), pageResource)); + } + + @Test + public void testPageNotModified() { + String pagePath = "/content/wknd/page"; + ZonedDateTime localTimestamp = ZonedDateTime.now().minusDays(1); + ZonedDateTime remoteTimestamp = ZonedDateTime.now().minusDays(2); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("jcr:primaryType", "cq:Page") + .add("lastModified", remoteTimestamp.toInstant().toEpochMilli()) + .build(); + + Page page = context.create().page(pagePath, null, Collections.singletonMap("cq:lastModified", GregorianCalendar.from(localTimestamp))); + Resource pageResource = page.adaptTo(Resource.class); + + assertFalse(updateStrategy.isModified(new CatalogItem(catalogItem), pageResource)); + } + + + @Test + public void testForwardRedirectServletToDefaultGetServlet() { + doAnswer(invocation -> { + GenericServlet servlet = mock(GenericServlet.class); + doReturn(REDIRECT_SERVLET).when(servlet).getServletName(); + return servlet; + }).when(servletResolver).resolveServlet(any(SlingHttpServletRequest.class)); + + String path = "/content/cq:tags"; + context.create().resource(path, "jcr:primaryType", "cq:Tag"); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", path); + + List items = updateStrategy.getItems(request); + assertEquals(1, items.size()); + CatalogItem item = items.iterator().next(); + assertEquals("/content/cq:tags.json", item.getContentUri()); + assertNull(item.getCustomExporter()); + } + + /** + * /conf/wknd/settings/wcm/templates/article-page-template/policies (cq:Page) export json rendered by DefaultGetServlet + * + jcr:content - export json rendered by ContentPolicyMappingServlet + */ + @Test + public void testCustomRendererUseParent() throws IOException { + String path = "/conf/wknd/settings/wcm/templates/article-page-template/policies"; + doAnswer(invocation -> { + SlingHttpServletRequest request = invocation.getArgument(0, SlingHttpServletRequest.class); + String resourcePath = request.getResource().getPath(); + GenericServlet servlet = mock(GenericServlet.class); + if (resourcePath.equals(path + "/jcr:content")) { + doReturn("com.day.cq.wcm.core.impl.policies.ContentPolicyMappingServlet").when(servlet).getServletName(); + } else if (resourcePath.equals(path)) { + doReturn(DEFAULT_GET_SERVLET).when(servlet).getServletName(); + } + return servlet; + }).when(servletResolver).resolveServlet(any(SlingHttpServletRequest.class)); + + context.create().page(path); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", path); + request.addRequestParameter("strategy", updateStrategy.getClass().getName()); + + List items = updateStrategy.getItems(request); + assertEquals(1, items.size()); + CatalogItem item = items.iterator().next(); + assertEquals("cq:Page", item.getPrimaryType()); + assertEquals(path + ".infinity.json", item.getContentUri()); + assertEquals(null, item.getCustomExporter()); + + } + + @Test + public void testCustomExporter() { + String path = "/content/wknd/page"; + String customExporter = "com.adobe.CustomJsonExporter"; + doAnswer(invocation -> { + GenericServlet servlet = mock(GenericServlet.class); + doReturn(customExporter).when(servlet).getServletName(); + return servlet; + }).when(servletResolver).resolveServlet(any(SlingHttpServletRequest.class)); + + context.create().page(path); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", path); + request.addRequestParameter("strategy", updateStrategy.getClass().getName()); + + List items = updateStrategy.getItems(request); + assertEquals(1, items.size()); + CatalogItem item = items.iterator().next(); + assertEquals("cq:Page", item.getPrimaryType()); + assertEquals(path + ".infinity.json", item.getContentUri()); + assertEquals(customExporter, item.getCustomExporter()); + } +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java new file mode 100644 index 0000000000..f802f77ae0 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java @@ -0,0 +1,151 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.contentsync.servlet; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class TestContentCatalogServlet { + @Rule + public AemContext context = new AemContext(ResourceResolverType.RESOURCEPROVIDER_MOCK); + + private ContentCatalogServlet servlet; + private UpdateStrategy updateStrategy; + + @Before + public void setUp() { + updateStrategy = mock(UpdateStrategy.class); + context.registerService(UpdateStrategy.class, updateStrategy); + servlet = context.registerInjectActivateService(new ContentCatalogServlet()); + } + + @Test + public void testMissingRequiredParameters() throws IOException { + doAnswer(invocation -> { + throw new IllegalArgumentException("root request parameter is required"); + }).when(updateStrategy).getItems(eq(context.request())); + + MockSlingHttpServletRequest request = context.request(); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals(SC_INTERNAL_SERVER_ERROR, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertEquals("root request parameter is required", jsonResponse.getString("error")); + } + + /** + * return an empty array if the requested path does not exist + */ + @Test + public void testContentTreeDoesNotExist() throws IOException { + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", "/content/wknd"); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals("application/json", response.getContentType()); + assertEquals(SC_OK, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertTrue("resources[] is missing in the response json", jsonResponse.containsKey("resources")); + + JsonArray resources = jsonResponse.getJsonArray("resources"); + + assertEquals(0, resources.size()); + } + + @Test + public void testPageTree() throws IOException { + doAnswer(invocation -> { + List items = new ArrayList<>(); + JsonObject o1 = Json.createObjectBuilder() + .add("path", "/content/wknd") + .add("jcr:primaryType", "cq:Page") + .build(); + JsonObject o2 = Json.createObjectBuilder() + .add("path", "/content/wknd/page1") + .add("jcr:primaryType", "cq:Page") + .build(); + items.add(new CatalogItem(o1)); + items.add(new CatalogItem(o2)); + return items; + }).when(updateStrategy).getItems(eq(context.request())); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", "/content/wknd"); + request.addRequestParameter("strategy", updateStrategy.getClass().getName()); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals("application/json", response.getContentType()); + assertEquals(SC_OK, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertTrue("resources[] is missing in the response json", jsonResponse.containsKey("resources")); + + JsonArray resources = jsonResponse.getJsonArray("resources"); + assertEquals(2, resources.size()); + + // first item is the root + JsonObject item1 = resources.getJsonObject(0); + assertEquals("/content/wknd", item1.getString("path")); + assertEquals("cq:Page", item1.getString("jcr:primaryType")); + + JsonObject item2 = resources.getJsonObject(1); + assertEquals("/content/wknd/page1", item2.getString("path")); + assertEquals("cq:Page", item2.getString("jcr:primaryType")); + } + + @Test + public void testInvalidStrategyPid() throws IOException { + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", "/content/wknd"); + request.addRequestParameter("strategy", "invalid"); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals("application/json", response.getContentType()); + assertEquals(SC_INTERNAL_SERVER_ERROR, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertTrue(jsonResponse.getString("error").startsWith("Cannot find UpdateStrategy for pid")); + } +} diff --git a/bundle/src/test/resources/contentsync/asset.json b/bundle/src/test/resources/contentsync/asset.json new file mode 100644 index 0000000000..ea0875de70 --- /dev/null +++ b/bundle/src/test/resources/contentsync/asset.json @@ -0,0 +1,1158 @@ +{ + "jcr:primaryType": "dam:AssetContent", + "jcr:mixinTypes": ["cq:ReplicationStatus"], + "cq:lastReplicationAction": "Activate", + "cq:lastReplicatedBy": "egor.kozlov", + "jcr:lastModifiedBy": "john.mcfarland@fleetcor.com", + "cq:lastReplicated": "2023-02-19T12:28:37.379Z", + "dam:assetState": "processed", + "jcr:lastModified": "2022-04-26T16:39:56.300Z", + "usages": { + "jcr:primaryType": "nt:unstructured", + "usedBy": ["asset"], + "dam:score": 1, + "asset": { + "jcr:primaryType": "nt:unstructured", + "lastUsed": "2022-05-16T03:49:08.558Z", + "count": 1 + } + }, + "renditions": { + "jcr:primaryType": "nt:folder", + "jcr:createdBy": "john.mcfarland@fleetcor.com", + "jcr:created": "2022-04-26T16:38:58.061Z", + "cq5dam.thumbnail.48.48.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.246Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-04-26T16:39:56.247Z", + ":jcr:data": 536 + } + }, + "cq5dam.thumbnail.140.100.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.242Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-04-26T16:39:56.243Z", + ":jcr:data": 1684 + } + }, + "cq5dam.web.1280.1280.jpeg": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.284Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/jpeg", + "jcr:lastModified": "2022-04-26T16:39:56.296Z", + ":jcr:data": 12627 + } + }, + "original": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "john.mcfarland@fleetcor.com", + "jcr:created": "2022-04-26T16:38:58.062Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "john.mcfarland@fleetcor.com", + "jcr:mimeType": "image/jpeg", + "jcr:lastModified": "2022-04-26T16:38:58.062Z", + ":jcr:data": 632024 + } + }, + "cq5dam.thumbnail.319.319.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.256Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-04-26T16:39:56.257Z", + ":jcr:data": 4082 + } + } + }, + "related": {"jcr:primaryType": "nt:unstructured"}, + "metadata": { + "jcr:primaryType": "nt:unstructured", + "jcr:mixinTypes": ["cq:Taggable"], + "dam:Physicalheightininches": "-1.0", + "dam:Physicalwidthininches": "-1.0", + "xmpTPg:HasVisibleTransparency": "False", + "dam:Fileformat": "JPEG", + "dam:Progressive": "no", + "tiff:ImageLength": 249, + "xmp:CreatorTool": "Adobe Illustrator 26.0 (Windows)", + "dam:extracted": "2022-04-26T16:39:56.083Z", + "dc:format": "image/jpeg", + "dam:Bitsperpixel": 32, + "xmpMM:DocumentID": "xmp.did:526773d2-a98f-654f-bf01-cbbe9b3a6f9d", + "dam:MIMEtype": "image/jpeg", + "dam:Physicalwidthindpi": -1, + "dam:Physicalheightindpi": -1, + "xmpMM:OriginalDocumentID": "uuid:5D20892493BFDB11914A8590D31508C8", + "ns1_1_:StartupProfile": "Print", + "xmpTPg:NPages": 1, + "dam:Numberofimages": 1, + "xmp:MetadataDate": "2020-05-26T16:53:03.000+05:30", + "xmpMM:RenditionClass": "proof:pdf", + "pdf:Producer": "Adobe PDF library 15.00", + "xmpTPg:HasVisibleOverprint": "False", + "ns1_1_:CreatorSubTool": "Adobe Illustrator", + "dam:Numberoftextualcomments": 0, + "xmpMM:InstanceID": "uuid:c9914b06-205d-4060-b620-fae57f2f5102", + "xmp:ModifyDate": "2021-12-10T14:22:37.000Z", + "xmp:CreateDate": "2021-12-10T08:22:37.000-06:00", + "tiff:ImageWidth": 657, + "dam:sha1": "08b44275cc46301a535ee1c7095c0daa3f3dffe8", + "dam:size": 632024, + "ns1_1_:Type": "Document", + "dc:title": "Print", + "xmp:Thumbnails": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 1, + "xmpArrayType": "rdf:Alt", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpGImg:image": "/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q==", + "xmpGImg:width": 256, + "xmpGImg:format": "JPEG", + "xmpGImg:height": 256 + } + }, + "xmpMM:DerivedFrom": { + "jcr:primaryType": "nt:unstructured", + "stRef:instanceID": "uuid:f346e470-2f42-4b41-ab9e-90c244b06151", + "stRef:originalDocumentID": "uuid:5D20892493BFDB11914A8590D31508C8", + "xmpNodeType": "xmpStruct", + "stRef:renditionClass": "proof:pdf", + "stRef:documentID": "xmp.did:68f615b9-b15d-024c-915a-8c6e37af3c70" + }, + "xmpMM:History": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 37, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-04-17T14:19:15.000+05:30", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:D27F11740720681191099C3B601C4548" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T16:23:06.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F1174072068118D4ED246B3ADB1C6" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T17:10:45.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FA7F1174072068118D4ED246B3ADB1C6" + }, + "7": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T22:53:33.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:EF7F117407206811A46CA4519D24356B" + }, + "8": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T23:07:07.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F07F117407206811A46CA4519D24356B" + }, + "9": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T10:35:43.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F117407206811BDDDFD38D0CF24DD" + }, + "10": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "11": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T10:40:59.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F117407206811BDDDFD38D0CF24DD" + }, + "12": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to ", + "xmpNodeType": "xmpStruct" + }, + "13": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:26:55.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FA7F117407206811BDDDFD38D0CF24DD" + }, + "14": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:29:01.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FB7F117407206811BDDDFD38D0CF24DD" + }, + "15": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:29:20.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FC7F117407206811BDDDFD38D0CF24DD" + }, + "16": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:30:54.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FD7F117407206811BDDDFD38D0CF24DD" + }, + "17": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:31:22.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FE7F117407206811BDDDFD38D0CF24DD" + }, + "18": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T12:23:46.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:B233668C16206811BDDDFD38D0CF24DD" + }, + "19": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T13:27:54.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:B333668C16206811BDDDFD38D0CF24DD" + }, + "20": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T13:46:13.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:B433668C16206811BDDDFD38D0CF24DD" + }, + "21": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T15:47:57.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F11740720681197C1BF14D1759E83" + }, + "22": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T15:51:06.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F87F11740720681197C1BF14D1759E83" + }, + "23": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T15:52:22.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F11740720681197C1BF14D1759E83" + }, + "24": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "25": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-22T13:28:01.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FA7F117407206811B628E3BF27C8C41B" + }, + "26": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "27": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-22T16:23:53.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FF7F117407206811B628E3BF27C8C41B" + }, + "28": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "29": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-28T16:45:26.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:07C3BD25102DDD1181B594070CEB88D9" + }, + "30": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "31": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-02T13:25:25.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F87F1174072068119098B097FDA39BEF" + }, + "32": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-09T14:58:36.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F117407206811BB1DBF8F242B6F84" + }, + "33": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-11T14:31:27.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F117407206811ACAFB8DA80854E76" + }, + "34": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-11T22:37:35.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:0180117407206811834383CD3A8D2303" + }, + "35": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-27T14:40:42.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F117407206811818C85DF6A1A75C3" + }, + "36": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS5", + "stEvt:when": "2009-11-26T17:25:36.000+05:30", + "stEvt:changed": "/", + "stEvt:instanceID": "xmp.iid:0580117407206811910989C2A6325BB5" + }, + "37": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator 24.2 (Windows)", + "stEvt:when": "2020-05-26T16:53:03.000+05:30", + "stEvt:changed": "/", + "stEvt:instanceID": "xmp.iid:526773d2-a98f-654f-bf01-cbbe9b3a6f9d" + } + }, + "xmpTPg:MaxPageSize": { + "jcr:primaryType": "nt:unstructured", + "stDim:h": "792.000000", + "stDim:w": "612.000000", + "xmpNodeType": "xmpStruct", + "stDim:unit": "Points" + }, + "xmpTPg:SwatchGroups": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 3, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpG:groupName": "Default Swatch Group", + "xmpG:groupType": 0, + "xmpG:Colorants": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 40, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "White", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "100.000000", + "xmpG:swatchName": "Black", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Red", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Yellow", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Green", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Cyan", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "7": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Blue", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "8": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Magenta", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "9": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=15 M=100 Y=90 K=10", + "xmpG:yellow": "90.000000", + "xmpG:cyan": "15.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "10": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "90.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=90 Y=85 K=0", + "xmpG:yellow": "85.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "11": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "80.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=80 Y=95 K=0", + "xmpG:yellow": "95.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "12": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=50 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "13": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "35.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=35 Y=85 K=0", + "xmpG:yellow": "85.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "14": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=5 M=0 Y=90 K=0", + "xmpG:yellow": "90.000000", + "xmpG:cyan": "5.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "15": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=20 M=0 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "20.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "16": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=50 M=0 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "17": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=75 M=0 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "75.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "18": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=85 M=10 Y=100 K=10", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "85.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "19": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "30.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "30.000000", + "xmpG:swatchName": "C=90 M=30 Y=95 K=30", + "xmpG:yellow": "95.000000", + "xmpG:cyan": "90.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "20": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=75 M=0 Y=75 K=0", + "xmpG:yellow": "75.000000", + "xmpG:cyan": "75.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "21": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=80 M=10 Y=45 K=0", + "xmpG:yellow": "45.000000", + "xmpG:cyan": "80.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "22": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "15.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=70 M=15 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "70.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "23": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=85 M=50 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "85.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "24": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "95.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=100 M=95 Y=5 K=0", + "xmpG:yellow": "5.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "25": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "25.000000", + "xmpG:swatchName": "C=100 M=100 Y=25 K=25", + "xmpG:yellow": "25.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "26": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=75 M=100 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "75.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "27": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=50 M=100 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "28": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=35 M=100 Y=35 K=10", + "xmpG:yellow": "35.000000", + "xmpG:cyan": "35.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "29": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=10 M=100 Y=50 K=0", + "xmpG:yellow": "50.000000", + "xmpG:cyan": "10.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "30": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "95.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=95 Y=20 K=0", + "xmpG:yellow": "20.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "31": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "25.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=25 M=25 Y=40 K=0", + "xmpG:yellow": "40.000000", + "xmpG:cyan": "25.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "32": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "45.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "5.000000", + "xmpG:swatchName": "C=40 M=45 Y=50 K=5", + "xmpG:yellow": "50.000000", + "xmpG:cyan": "40.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "33": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "25.000000", + "xmpG:swatchName": "C=50 M=50 Y=60 K=25", + "xmpG:yellow": "60.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "34": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "60.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "40.000000", + "xmpG:swatchName": "C=55 M=60 Y=65 K=40", + "xmpG:yellow": "65.000000", + "xmpG:cyan": "55.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "35": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "40.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=25 M=40 Y=65 K=0", + "xmpG:yellow": "65.000000", + "xmpG:cyan": "25.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "36": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=30 M=50 Y=75 K=10", + "xmpG:yellow": "75.000000", + "xmpG:cyan": "30.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "37": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "60.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "25.000000", + "xmpG:swatchName": "C=35 M=60 Y=80 K=25", + "xmpG:yellow": "80.000000", + "xmpG:cyan": "35.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "38": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "65.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "35.000000", + "xmpG:swatchName": "C=40 M=65 Y=90 K=35", + "xmpG:yellow": "90.000000", + "xmpG:cyan": "40.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "39": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "70.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "50.000000", + "xmpG:swatchName": "C=40 M=70 Y=100 K=50", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "40.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "40": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "70.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "70.000000", + "xmpG:swatchName": "C=50 M=70 Y=80 K=70", + "xmpG:yellow": "80.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + } + } + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpG:groupName": "Grays", + "xmpG:groupType": 1, + "xmpG:Colorants": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 11, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "100.000000", + "xmpG:swatchName": "C=0 M=0 Y=0 K=100", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "89.999400", + "xmpG:swatchName": "C=0 M=0 Y=0 K=90", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "79.998800", + "xmpG:swatchName": "C=0 M=0 Y=0 K=80", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "69.999700", + "xmpG:swatchName": "C=0 M=0 Y=0 K=70", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "59.999100", + "xmpG:swatchName": "C=0 M=0 Y=0 K=60", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "50.000000", + "xmpG:swatchName": "C=0 M=0 Y=0 K=50", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "7": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "39.999400", + "xmpG:swatchName": "C=0 M=0 Y=0 K=40", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "8": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "29.998800", + "xmpG:swatchName": "C=0 M=0 Y=0 K=30", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "9": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "19.999700", + "xmpG:swatchName": "C=0 M=0 Y=0 K=20", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "10": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "9.999100", + "xmpG:swatchName": "C=0 M=0 Y=0 K=10", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "11": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "4.998800", + "xmpG:swatchName": "C=0 M=0 Y=0 K=5", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + } + } + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpG:groupName": "Brights", + "xmpG:groupType": 1, + "xmpG:Colorants": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 6, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=100 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "75.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=75 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=10 Y=95 K=0", + "xmpG:yellow": "95.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=85 M=10 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "85.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "90.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=100 M=90 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "90.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.003100", + "xmpG:swatchName": "C=60 M=90 Y=0 K=0", + "xmpG:yellow": "0.003100", + "xmpG:cyan": "60.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + } + } + } + } + } + } \ No newline at end of file diff --git a/bundle/src/test/resources/contentsync/jcr_content.json b/bundle/src/test/resources/contentsync/jcr_content.json new file mode 100644 index 0000000000..a25daa4f59 --- /dev/null +++ b/bundle/src/test/resources/contentsync/jcr_content.json @@ -0,0 +1,107 @@ +{ + "jcr:primaryType": "cq:PageContent", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:title": "Test", + "cq:lastReplicationAction": "Activate", + "jcr:versionHistory": "cf2b4a68-8420-4c76-9e1a-c233e77de565", + "cq:lastReplicatedBy": "admin", + "jcr:predecessors": [ + "621ac7f5-949e-4feb-9bdc-7cb5b1914dce" + ], + "jcr:created": "2023-06-29T19:12:49.925+03:00", + "cq:lastReplicated": "2023-07-03T13:16:01.601+03:00", + "cq:lastModified": "2022-12-19T12:54:37.988Z", + "sling:redirectStatus": 302, + "jcr:baseVersion": "621ac7f5-949e-4feb-9bdc-7cb5b1914dce", + "jcr:isCheckedOut": true, + "cq:conf": "/conf/test", + "acs:unknown": "unknown namespace. to be sanitized", + ":customBinaryProperty": 100, + "jcr:uuid": "5ec431ff-d63c-4459-a072-ddaeb7519d4f", + "sling:resourceType": "foundation/components/redirect", + "cq:allowedTemplates": [ + "/apps/test/templates/.*", + "/conf/test/settings/wcm/templates/.*" + ], + "cq:designPath": "/etc/designs/test", + "cq:lastModifiedBy": "vadim.filatov", + "image": { + "jcr:primaryType": "nt:unstructured", + "file": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.732+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:mixinTypes": [ + "dam:Thumbnails" + ], + "jcr:lastModifiedBy": "vadim.filatov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-12-19T12:54:32.332Z", + ":jcr:data": 1518, + "jcr:uuid": "6866ac8a-3d69-4be7-a5ea-1be1d26bc170", + "dam:thumbnails": { + "jcr:primaryType": "nt:folder", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.733+03:00", + "dam:thumbnail_480.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.733+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "egor.kozlov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-24T10:36:11.395Z", + ":jcr:data": 1162, + "jcr:uuid": "ad554ca4-3e9b-4742-80c3-3cb0d6b465e6" + } + }, + "dam:thumbnail_60.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.733+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "vadim.filatov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-26T12:32:39.157Z", + ":jcr:data": 1556, + "jcr:uuid": "4f06e735-8761-48d6-9d62-1a695d61fe68" + } + }, + "dam:thumbnail_300.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.734+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "egor.kozlov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-24T10:36:11.395Z", + ":jcr:data": 1162, + "jcr:uuid": "a342e1bf-2f36-459e-be09-0c1a732f848b" + } + }, + "dam:thumbnail_48.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.734+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "egor.kozlov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-24T10:36:11.396Z", + ":jcr:data": 1230, + "jcr:uuid": "880abe33-61ce-4176-83b0-56267fa1f44f" + } + } + } + } + } + } +} diff --git a/bundle/src/test/resources/contentsync/ordered-folder.json b/bundle/src/test/resources/contentsync/ordered-folder.json new file mode 100644 index 0000000000..207e9aff06 --- /dev/null +++ b/bundle/src/test/resources/contentsync/ordered-folder.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "sling:OrderedFolder", + "jcr:createdBy": "john.doe", + "jcr:title": "Wknd Fragments", + "cq:adobeTargetExportFormat": "html", + "jcr:created": "2021-09-02T15:06:55.647Z", + "cq:allowedTemplates": ["/conf/wknd/settings/wcm/templates/(?!page-).*"] + } \ No newline at end of file diff --git a/bundle/src/test/resources/contentsync/riverside-camping-australia.1.json b/bundle/src/test/resources/contentsync/riverside-camping-australia.1.json new file mode 100644 index 0000000000..ad79931a24 --- /dev/null +++ b/bundle/src/test/resources/contentsync/riverside-camping-australia.1.json @@ -0,0 +1,115 @@ +{ + "jcr:primaryType": "sling:OrderedFolder", + "jcr:createdBy": "admin", + "jcr:created": "2023-06-07T16:09:43.164+03:00", + "adobestock-216674449.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "e07e4951-b3a1-495d-886c-97a1648e0596", + "jcr:predecessors": [ + "72a074a5-3eab-47a1-acff-153e35251e76" + ], + "jcr:created": "2023-06-07T16:10:27.576+03:00", + "jcr:baseVersion": "72a074a5-3eab-47a1-acff-153e35251e76", + "jcr:isCheckedOut": true, + "jcr:uuid": "26227675-38fa-447a-9bf0-cef87b59e227" + }, + "jcr:content": { + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Riverside Camping Australia", + "cq:isTransCreated": true + }, + "adobe_waadobe_wa_mg_2466.jpg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "976af8b1-3afc-40ac-a164-ab4b16a8c8ef", + "jcr:predecessors": [ + "d56cedaf-53a3-414f-a28d-11f67338594f" + ], + "jcr:created": "2023-06-07T16:10:28.033+03:00", + "jcr:baseVersion": "d56cedaf-53a3-414f-a28d-11f67338594f", + "jcr:isCheckedOut": true, + "jcr:uuid": "ac3a0532-8823-4950-8209-26d8c0a35b92" + }, + "riverside-camping-australia": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "37f52909-cd78-480e-912b-ccf313e275ec", + "jcr:predecessors": [ + "68190a39-93f4-4f5a-abe5-9602620736bc" + ], + "jcr:created": "2023-06-07T16:10:27.823+03:00", + "jcr:baseVersion": "68190a39-93f4-4f5a-abe5-9602620736bc", + "jcr:isCheckedOut": true, + "jcr:uuid": "9ead3982-0245-497f-97b0-cdf1b50f4534" + }, + "adobestock-257541512.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "17be11ee-7a05-4571-ac67-f417f85599c3", + "jcr:predecessors": [ + "21cfedac-2838-4351-a4e5-e4ca0842dc5e" + ], + "jcr:created": "2023-06-07T16:10:27.257+03:00", + "jcr:baseVersion": "21cfedac-2838-4351-a4e5-e4ca0842dc5e", + "jcr:isCheckedOut": true, + "jcr:uuid": "700f4628-f618-487a-b4ad-ebf25e8edaec" + }, + "adobestock-238491803.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "e338bb1f-42ce-430c-ad2c-e09500676c8f", + "jcr:predecessors": [ + "7c71b8c4-02a2-4677-bb9d-4e7823895c44" + ], + "jcr:created": "2023-06-07T16:10:26.471+03:00", + "jcr:baseVersion": "7c71b8c4-02a2-4677-bb9d-4e7823895c44", + "jcr:isCheckedOut": true, + "jcr:uuid": "1d32c79d-f849-4a34-8a6d-2bd7cb6635a3" + }, + "adobestock-178022573.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "fa58100d-23ef-4e65-9e94-e8223c061155", + "jcr:predecessors": [ + "e4b3c82a-0b21-4c4d-9f79-9c36ceefde27" + ], + "jcr:created": "2023-06-07T16:10:26.828+03:00", + "jcr:baseVersion": "e4b3c82a-0b21-4c4d-9f79-9c36ceefde27", + "jcr:isCheckedOut": true, + "jcr:uuid": "50ab0979-d7b5-486a-a303-8d876e4a96a4" + }, + "adobestock-167833331.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "d763e50b-14cf-452b-b93f-42614fb1eb30", + "jcr:predecessors": [ + "37bb8525-cfdf-40ac-8078-655aed478dd2" + ], + "jcr:created": "2023-06-07T16:10:25.988+03:00", + "jcr:baseVersion": "37bb8525-cfdf-40ac-8078-655aed478dd2", + "jcr:isCheckedOut": true, + "jcr:uuid": "bc596a32-f7c1-4b0a-8268-cca3e0dbc58e" + } +} \ No newline at end of file diff --git a/bundle/src/test/resources/contentsync/wknd-faqs.json b/bundle/src/test/resources/contentsync/wknd-faqs.json new file mode 100644 index 0000000000..e20d95d1f4 --- /dev/null +++ b/bundle/src/test/resources/contentsync/wknd-faqs.json @@ -0,0 +1,301 @@ +{ + "jcr:primaryType": "cq:PageContent", + "jcr:mixinTypes": [], + "jcr:createdBy": "admin", + "jcr:title": "FAQs", + "cq:template": "/conf/wknd/settings/wcm/templates/content-page-template", + "jcr:created": "2023-07-03T11:58:31.272+03:00", + "cq:lastModified": "2020-07-07T17:58:17.582-07:00", + "jcr:description": "Here we answer some of the questions that we hear most often from the community.", + "cq:tags": [], + "sling:resourceType": "wknd/components/page", + "cq:lastModifiedBy": "admin", + "root": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "wknd/components/container", + "container": { + "jcr:primaryType": "nt:unstructured", + "jcr:lastModifiedBy": "admin", + "layout": "responsiveGrid", + "jcr:lastModified": "2020-07-07T17:56:59.701-07:00", + "sling:resourceType": "wknd/components/container", + "cq:styleIds": ["1554340406437"], + "container": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "layout": "responsiveGrid", + "jcr:lastModified": "2020-07-07T17:57:13.729-07:00", + "sling:resourceType": "wknd/components/container", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "8" + }, + "tablet": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + }, + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + }, + "title": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-14T09:28:37.553-07:00", + "sling:resourceType": "wknd/components/title", + "cq:styleIds": ["1568996484405"], + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + }, + "image": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "fileReference": "/content/dam/wknd-shared/en/adventures/climbing-new-zealand/adobestock-277768563.jpeg", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-25T16:02:31.787-07:00", + "sling:resourceType": "wknd/components/image", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "8" + } + } + }, + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND is a collective of outdoors, music, crafts, adventure sports, and travel enthusiasts that want to share our experiences, connections, and expertise with the world. Our objective is create a community to help like-minded adventure seekers find fun, engaging, and responsible ways to to enjoy life and create lasting memories.

\n", + "jcr:lastModified": "2019-10-25T11:13:10.055-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + }, + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "8" + } + } + }, + "accordion": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "headingElement": "h3", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-25T11:15:13.801-07:00", + "singleExpansion": "false", + "sling:resourceType": "wknd/components/accordion", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + }, + "item_1571070731882": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "Who is WKND's intended audience?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

We believe the best adventures and activities are those that are accessible to everyone. WKND is designed to be inclusive of all age ranges, abilities, and budget-levels. We strive to cater to the thrill-seeking adrenaline junkie BASE-jumpers as well as novices that have a spare weekend and interest in trying something new.    

\n", + "jcr:lastModified": "2019-10-25T11:16:36.508-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1": { + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 1", + "cq:panelTitle": "How does WKND pay for itself?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND charges a small fee for local promoters that want to sponsor their adventures and events on the WKND site.  Sponsored Adventures may get sorted to more prominent positions in our Adventures listings pages.

\n

 

\n", + "jcr:lastModified": "2019-10-25T11:17:16.517-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_2": { + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 2", + "cq:panelTitle": "Can I contribute to WKND?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

Yes!  If you have the expertise and experiences to share, we’ll provide the platform to spread it.  As a Guest Writer, you will play an integral role in helping people find fun and cool things to do in your community.

\n", + "jcr:lastModified": "2019-10-25T11:17:39.663-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1571070705187": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "How often is WKDN updated?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND is updated daily to provide you with the latest in-depth articles on fun activities that we’ve recently exploring and new adventures that are available for you to discover.  Come back often to see the latest or subscribe to our social feeds.

\n", + "jcr:lastModified": "2019-10-25T11:18:11.562-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1571070718887": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "When was WKND founded?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND was created in 2015 when our founders, Daniel and Kilian, realized that their friends and family were constantly using them as resources to find fun things to do while they were in Los Angeles. They loved sharing ideas about fun events and activities they knew of, but wanted to be able to do it at larger scale across communities.  They decided to start WKND as a way to share their insights and experiences with as many people as possible.    

\n", + "jcr:lastModified": "2019-10-25T11:18:36.836-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1572027287775": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "Is a hot dog a sandwich?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

While it may be described as meat between two pieces of bread, a hot dog is just a sandwich in the same way Michael Jordan was just a basketball player or William Shakespeare was just a playwright.  Technically true, but vastly understated.

\n", + "jcr:lastModified": "2019-10-25T11:19:08.411-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1572027298182": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "Is WKND a real company?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

No. The WKND is a fictional online magazine and adventure company that focuses on outdoor activities and trips across the globe. The WKND site is designed to demonstrate functionality for Adobe Experience Manager. There is also a corresponding tutorial that walks a developer through the development.  Special thanks to Lorenzo Buosi and Kilian Amendola who created the beautiful design for the WKND site.

\n", + "jcr:lastModified": "2019-10-25T11:19:41.501-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + } + } + } + }, + "container_293505757": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-14T10:20:06.270-07:00", + "sling:resourceType": "wknd/components/container", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "1", + "width": "3" + }, + "tablet": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + }, + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + }, + "separator": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-14T13:49:29.760-07:00", + "sling:resourceType": "wknd/components/separator", + "cq:styleIds": [ + "1571075919520", + "1571075910473" + ] + }, + "title": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:title": "Need more help?", + "jcr:lastModifiedBy": "admin", + "type": "h3", + "jcr:lastModified": "2019-10-14T10:21:13.842-07:00", + "sling:resourceType": "wknd/components/title", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + }, + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

Give us a call at 1-800-800-0000.
\r\nE-mail us at info@wknd.com
\r\nWe love to talk adventures!

\r\n", + "jcr:lastModified": "2019-10-14T10:31:09.107-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true", + "cq:styleIds": ["1571074070335"], + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + } + } + } + } + } \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp new file mode 100755 index 0000000000..11c893a26f --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp @@ -0,0 +1,254 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" contentType="text/html; charset=utf-8" + pageEncoding="UTF-8" + import=" + java.util.List, + java.util.ArrayList, + java.util.Collection, + java.util.Set, + java.util.LinkedHashSet, + java.util.stream.Collectors, + java.io.InputStream, + java.io.IOException, + java.io.PrintWriter, + java.io.ByteArrayInputStream, + javax.jcr.Session, + javax.json.Json, + javax.json.JsonArray, + javax.json.JsonObject, + org.apache.sling.jcr.contentloader.ContentImporter, + org.apache.sling.api.resource.ResourceUtil, + org.apache.commons.lang3.time.DurationFormatUtils, + com.adobe.acs.commons.contentsync.* + +"%><% +%> + + + + + +
+<%
+
+    String root = request.getParameter("root");
+    String cfgPath = request.getParameter("source");
+
+    boolean dryRun = request.getParameter("dryRun") != null;
+    String workflowModel = request.getParameter("workflowModel");
+	boolean incremental = request.getParameter("incremental") != null;
+	boolean createVersion = request.getParameter("createVersion") != null;
+	boolean delete = request.getParameter("delete") != null;
+
+    ValueMap generalSettings = ConfigurationUtils.getSettingsResource(resourceResolver).getValueMap();
+
+    String observationData = generalSettings.get(ConfigurationUtils.EVENT_USER_DATA_KEY, String.class);
+    String strategyPid = generalSettings.get(ConfigurationUtils.UPDATE_STRATEGY_KEY, String.class);
+
+	Resource cfg = resourceResolver.getResource(cfgPath);
+    SyncHostConfiguration hostConfig = cfg.adaptTo(SyncHostConfiguration.class);
+
+    if(hostConfig == null || hostConfig.getHost().isEmpty()){
+        error(out, "Configure AEM Environments");
+        return;
+    }
+
+    String catalogServlet = slingRequest.getResource().getPath() + ".catalog.json";
+    Session session = resourceResolver.adaptTo(Session.class);
+    ContentReader contentReader = new ContentReader(session);
+
+    UpdateStrategy updateStrategy = sling.getServices(UpdateStrategy.class, "(component.name=" + strategyPid + ")")[0];
+    try(RemoteInstance remoteInstance = new RemoteInstance(hostConfig)){
+        ContentImporter importer = sling.getService(ContentImporter.class);
+        ContentSync contentSync = new ContentSync(remoteInstance, resourceResolver, importer);
+        ContentCatalog contentCatalog = new ContentCatalog(remoteInstance, catalogServlet);
+
+        out.println("building catalog from " + contentCatalog.getFetchURI(root, strategyPid) );
+        out.flush();
+        List catalog;
+        List remoteItems = contentCatalog.fetch(root, strategyPid);
+        long t0 = System.currentTimeMillis();
+        out.println(remoteItems.size() + " resource"+(remoteItems.size() == 1 ? "" : "s")+" fetched in " + (System.currentTimeMillis() - t0) + " ms");
+        if(incremental){
+            catalog = contentCatalog.getDelta(remoteItems, resourceResolver, updateStrategy);
+            out.println(catalog.size() + " resource"+(catalog.size() == 1 ? "" : "s")+" modified");
+        } else {
+            catalog = remoteItems;
+        }
+
+        long count = 0;
+        t0 = System.currentTimeMillis();
+        long t00 = System.currentTimeMillis();
+        List updatedResources = new ArrayList<>();
+
+        // the list of updated resources having child nodes to ensure ordering after update
+        Set sortedNodes = new LinkedHashSet<>();
+
+        for (CatalogItem item : catalog) {
+            String path = item.getPath();
+            String customExporter = item.getCustomExporter();
+            if(customExporter != null){
+                error(out, "\t" + path + " has a custom json exporter (" + customExporter + ") and cannot be imported");
+                continue;
+            }
+
+
+            Resource targetResource = resourceResolver.getResource(path);
+
+            boolean modified = updateStrategy.isModified(item, targetResource);
+
+            if(targetResource == null || modified || !incremental) {
+                out.println(++count + "\t" + path);
+                String msg = updateStrategy.getMessage(item, targetResource);
+                out.println("\t" + msg);
+                if(!dryRun) {
+                    String reqPath = item.getContentUri() ;
+                    JsonObject json = remoteInstance.getJson(reqPath);
+
+                    List binaryProperties = contentReader.collectBinaryProperties(json);
+                    JsonObject sanitizedJson = contentReader.sanitize(json);
+
+                    if(targetResource != null && createVersion) {
+                        String revisionId = contentSync.createVersion(targetResource);
+                        if(revisionId != null) {
+                            out.println("\tcreated revision: " + revisionId);
+                        }
+                    }
+                    if(observationData != null){
+                        session.getWorkspace().getObservationManager().setUserData(observationData);
+                    }
+
+                    out.println("\timporting data");
+                    contentSync.importData(item, sanitizedJson);
+
+                    if(!binaryProperties.isEmpty()){
+                        out.println("\tcopying " + binaryProperties.size() + " binary propert" + (binaryProperties.size() > 1 ? "ies" : "y"));
+                        boolean contentResource = item.hasContentResource();
+                        String basePath = path + (contentResource ? "/jcr:content" : "");
+                        List propertyPaths = binaryProperties.stream().map(p -> basePath + p).collect(Collectors.toList());
+                        contentSync.copyBinaries(propertyPaths);
+                    }
+
+			        String parentPath = ResourceUtil.getParent(path);
+                    if(parentPath.startsWith(root)){
+                        sortedNodes.add(parentPath);
+                    }
+
+                    if(observationData != null){
+                        session.getWorkspace().getObservationManager().setUserData(observationData);
+                    }
+
+                    session.save();
+
+                    // print ETA every 5 seconds
+                    if(System.currentTimeMillis() - t00 > 5000L){
+                        long remainingCycles = catalog.size() - count;
+                        long pace = (System.currentTimeMillis()-t0)/count;
+                        long estimatedTime = remainingCycles * pace ;
+                        String pct = String.format("%.0f", count*100./catalog.size());
+                        String eta = DurationFormatUtils.formatDurationWords(estimatedTime, true, true);
+                        String etaMsg = pct +"%, ETA: " + eta;
+                        t00 = System.currentTimeMillis();
+                        out.println(etaMsg);
+                    }
+
+                    updatedResources.add(path);
+
+                    out.flush();
+                }
+            }
+        }
+
+        if(delete){
+            Collection remotePaths = remoteItems.stream().map(c -> c.getPath()).collect(Collectors.toList());
+            Collection localPaths = updateStrategy.getItems(slingRequest).stream().map(c -> c.getPath()).collect(Collectors.toList());
+            localPaths.removeAll(remotePaths);
+            out.println();
+            for(String path : localPaths){
+				Resource res = resourceResolver.getResource(path);
+                if(res != null){
+                	out.println("deleting " + path);
+                    if(!dryRun) {
+                        if(res != null) {
+                            resourceResolver.delete(res);
+                        }
+                    }
+                }
+            }
+        }
+
+        out.println();
+        for(String parentPath : sortedNodes){
+            Node targetNode = resourceResolver.getResource(parentPath).adaptTo(Node.class);
+            out.println("sorting child nodes of " + targetNode.getPath() );
+            contentSync.sort(targetNode);
+        }
+        session.save();
+
+        out.println();
+        out.println("sync-ed " + count + " resources, in " + (System.currentTimeMillis() - t0) + " ms");
+
+        if(!dryRun && workflowModel != null && !workflowModel.isEmpty()){
+	        out.println();
+            long t1 = System.currentTimeMillis();
+
+            out.println("starting a " + workflowModel + " workflow for each processed item");
+            out.flush();
+            contentSync.runWorkflows(workflowModel, updatedResources);
+	        out.println("started " + updatedResources.size() + " workflows, in " + (System.currentTimeMillis() - t1) + " ms");
+        }
+
+    } catch(Exception e){
+        if(e.getMessage() != null && e.getMessage().startsWith("Not a date string:")){
+            error(out, "It appears Sling GET Servlet on " + hostConfig.getHost() + " is configured to use the legacy ECMA date format.\n" +
+                  "Please edit configuration for PID org.apache.sling.servlets.get.DefaultGetServlet and make sure 'Enable legacy Sling ECMA format for dates' is unchecked.");
+        }
+        error(out, e);
+    }
+
+
+%>
+
+ + +<%! + + void error(JspWriter out, String msg) throws IOException { + out.print(""); + out.print(msg); + out.println(""); + } + + void error(JspWriter out, Throwable e) throws IOException { + out.print(""); + e.printStackTrace(new PrintWriter(out)); + out.println(""); + } +%> \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml new file mode 100755 index 0000000000..fc394d2f46 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml @@ -0,0 +1,5 @@ + + diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt new file mode 100755 index 0000000000..2b43fff6c1 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt @@ -0,0 +1,3 @@ +#base=css + +app.css \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css new file mode 100755 index 0000000000..332077e510 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css @@ -0,0 +1,13 @@ +.diagnosis-panel-with-margin { + margin-top: 1rem; + margin-left: 3rem; + margin-right: 3rem; +} + +.panelWithMarginTop { + margin-top: 1.5rem; +} + +.checkboxes-panel { + column-width: 400px; +} \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt new file mode 100755 index 0000000000..149ad78246 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt @@ -0,0 +1,3 @@ +#base=js + +app.js \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js new file mode 100755 index 0000000000..1bac84de12 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js @@ -0,0 +1,72 @@ +/* + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + */ +(function(document, $, Granite) { + "use strict"; + + var ui = $(window).adaptTo("foundation-ui"); + + $(window).adaptTo("foundation-registry").register("foundation.collection.action.action", { + name: "acs-commons.dashboard.task.delete", + handler: function(name, el, config, collection, selections) { + if (selections.length === 1) { + var card = $(selections).find("coral-card"); + if(card.length > 0) { + var taskName = card.data("host"); + var message = "You are going to delete the following item:" + + "

" + taskName + "

"; + + ui.prompt("Delete Host", message, "notice", [ + { + text: "Cancel" + }, + { + text: "Delete", + warning: true, + handler: function() { + selections.map(function(item) { + return $.ajax({ + type: "DELETE", + url: $(item).find("coral-card").data("path") + }).then(function() { + window.location.reload(true); + }); + }); + } + } + ]); + } + } + } + }); + + $(document).on("click", ".configureQuickAction", function(e) { + var form = $(this).closest("coral-masonry-item").find("coral-card"); + var host = $(form).data("host"); + var username = $(form).data("username"); + var password = $(form).data("password"); + var action = $(form).data("path"); + var dlg = document.querySelector('#modalConfigureHost'); + $("#configureHost-host").val(host); + $("#configureHost-username").val(username); + $("#configureHost-password").val(password); + $(dlg).find("#createHostForm").attr("action", action); + dlg.show(); + }); + + +})(document, Granite.$); diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp new file mode 100755 index 0000000000..2754504f06 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp @@ -0,0 +1,38 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" import=" + com.adobe.granite.ui.components.PagingIterator, + com.adobe.granite.ui.components.ds.DataSource, + com.adobe.granite.ui.components.ds.AbstractDataSource, + java.util.Iterator, + com.adobe.acs.commons.contentsync.ConfigurationUtils + " %><% + + // ensure the settings nodes + ConfigurationUtils.getSettingsResource(resourceResolver); + + Resource hostsResource = ConfigurationUtils.getHostsResource(resourceResolver); + DataSource ds = new AbstractDataSource() { + public Iterator iterator() { + return new PagingIterator(hostsResource.listChildren(), null, null); + } + }; + + request.setAttribute(DataSource.class.getName(), ds); +%> diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp new file mode 100755 index 0000000000..cf284f193e --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp @@ -0,0 +1,73 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/granite/ui/global.jsp"%><% +%><%@page session="false" import=" + org.apache.sling.api.resource.ValueMap, + com.adobe.granite.ui.components.Tag, + com.adobe.granite.ui.components.AttrBuilder" %><% + + String contextPath = request.getContextPath(); + ValueMap valueMap = resource.adaptTo(ValueMap.class); + String username = valueMap.get("username", ""); + String password = valueMap.get("password", ""); + String url = valueMap.get("host", ""); + String icon = "viewList"; + String smallIcon = "search"; + String path = resource.getPath(); + String href = "/apps/acs-commons/content/contentsync.html" + path; + + Tag tag = cmp.consumeTag(); + AttrBuilder attrs = tag.getAttrs(); + + attrs.addClass("foundation-collection-navigator"); + attrs.addClass("whitecard"); +%> + + data-foundation-collection-navigator-href="<%= href %>" + data-host="<%=url%>" + data-username="<%=username%>" + data-password="<%=password%>" + data-path="<%=path%>" + colorhint="#FFFFFF"> + + + + + + + + + + + +
+
+ + +
+
+ <%= url %> +
+
+
+
+ + Select + Configure + diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml new file mode 100755 index 0000000000..491392d539 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp new file mode 100755 index 0000000000..95e60b52f0 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp @@ -0,0 +1,36 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/granite/ui/global.jsp" %><% +%><%@page session="false" + import="java.util.Iterator, + com.adobe.granite.ui.components.AttrBuilder, + com.adobe.granite.ui.components.Config, + com.adobe.granite.ui.components.Tag, + com.adobe.granite.ui.components.Value" %><% +Tag tag = cmp.consumeTag(); +AttrBuilder attrs = tag.getAttrs(); +cmp.populateCommonAttrs(attrs); +Config cfg = cmp.getConfig(); +attrs.add("name", cfg.get("granite:id", String.class)); + +%> +
+ +
diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp new file mode 100755 index 0000000000..4edac63bb2 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp @@ -0,0 +1,51 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" import=" + org.apache.sling.api.resource.ResourceMetadata, + org.apache.sling.api.wrappers.ValueMapDecorator, + java.util.List, + java.util.HashMap, + java.util.ArrayList, + com.adobe.granite.ui.components.ds.DataSource, + com.adobe.granite.ui.components.ds.SimpleDataSource, + com.adobe.granite.ui.components.ds.ValueMapResource, + com.adobe.acs.commons.contentsync.ConfigurationUtils + + " %><% + + Resource hostsResource = ConfigurationUtils.getHostsResource(resourceResolver); + Resource suffixResource = slingRequest.getRequestPathInfo().getSuffixResource(); + + List lst = new ArrayList(); + if(hostsResource != null) { + for (Resource item : hostsResource.getChildren()) { + ValueMap vm = new ValueMapDecorator(new HashMap()); + + String host = item.getValueMap().get("host", ""); + vm.put("value", item.getPath()); + vm.put("text", host); + if(suffixResource != null && suffixResource.getPath().equals(item.getPath())) { + vm.put("selected", true); + } + + lst.add(new ValueMapResource(resourceResolver, new ResourceMetadata(), "nt:unstructured", vm)); + } + } + request.setAttribute(DataSource.class.getName(), new SimpleDataSource(lst.iterator())); +%> \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp new file mode 100755 index 0000000000..602faaf209 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp @@ -0,0 +1,44 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" import=" + java.util.List, + java.util.ArrayList, + java.util.HashMap, + org.apache.sling.api.resource.ResourceMetadata, + org.apache.sling.api.wrappers.ValueMapDecorator, + com.adobe.granite.ui.components.ds.DataSource, + com.adobe.granite.ui.components.ds.SimpleDataSource, + com.adobe.granite.ui.components.ds.ValueMapResource, + com.adobe.acs.commons.contentsync.UpdateStrategy"%><% +%><% + + List lst = new ArrayList(); + UpdateStrategy[] s = sling.getServices(UpdateStrategy.class, null); + if(s != null) { + for (UpdateStrategy stragegy : s) { + ValueMap vm = new ValueMapDecorator(new HashMap()); + + vm.put("value", stragegy.getClass().getName()); + vm.put("text", stragegy.getClass().getName()); + + lst.add(new ValueMapResource(resourceResolver, new ResourceMetadata(), "nt:unstructured", vm)); + } + } + request.setAttribute(DataSource.class.getName(), new SimpleDataSource(lst.iterator())); +%> \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml new file mode 100755 index 0000000000..07a5fb73a9 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml @@ -0,0 +1,130 @@ + + + + + + + + + <actions jcr:primaryType="nt:unstructured"> + <secondary jcr:primaryType="nt:unstructured"> + <configure + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + href="/apps/acs-commons/content/contentsync/configure.html" + icon="gear" + text="Configure" + variant="primary"/> + </secondary> + </actions> + <content + granite:class="diagnosis-panel-with-margin" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/container"> + <items jcr:primaryType="nt:unstructured"> + <form + granite:class="transform-form" + granite:id="transform-form" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form" + action="/apps/acs-commons/content/contentsync/jcr:content/sync.html" + foundation-form="{Boolean}true" + method="POST" + style="vertical" + target="replication_status_iframe"> + <items jcr:primaryType="nt:unstructured"> + <path + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/pathfield" + fieldLabel="Path to synchronize" + filter="hierarchyNotFile" + name="root" + required="{Boolean}true" + rootPath="/content" + value="/content/my-site"/> + <host + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/select" + fieldLabel="Source environment" + name="source" + required="{Boolean}true"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/selecthostdatasource"/> + </host> + <workflow + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/select" + emptyText="Select one" + fieldLabel="On-complete workflow " + ignoreData="{Boolean}true" + name="workflowModel"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="cq/gui/components/coral/common/admin/timeline/events/workflow/datasources/models"/> + </workflow> + <checkboxes + granite:class="checkboxes-panel" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/container"> + <items jcr:primaryType="nt:unstructured"> + <incremental + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + checked="{Boolean}true" + name="incremental" + text="Incremental update. Will only copy new and changed resources" + value="true"/> + <dryRun + granite:class="notice-wide" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + checked="{Boolean}true" + name="dryRun" + text="Dry Run" + value="false"/> + <revision + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + name="createVersion" + text="Create revision before update" + value="true"/> + <delete + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + name="delete" + text="Delete files that exist in the destination but not in the source" + value="true"/> + </items> + </checkboxes> + <submit + granite:id="executeQueryButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + text="Submit" + type="submit" + variant="primary"/> + <iframe + granite:id="replication_status_iframe" + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/iframe"/> + </items> + </form> + </items> + </content> + <sync + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync"/> + </jcr:content> +</jcr:root> diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml new file mode 100755 index 0000000000..e1f3aa870d --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml @@ -0,0 +1,275 @@ +<?xml version="1.0" encoding="UTF-8"?> +<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" + jcr:primaryType="cq:Page"> + <jcr:content + jcr:primaryType="nt:unstructured" + jcr:title="Content Sync - Configure AEM Environments" + sling:resourceType="granite/ui/components/shell/collectionpage" + consoleId="contentsyncHosts" + contentPath="/content" + currentView="${state["shell.collectionpage.layoutId"].string}" + modeGroup="contentsync-hosts-task-collection" + pageURITemplate="/libs/granite/operations/content/diagnosis.html" + targetCollection=".contentsync-hosts-task-collection"> + <head jcr:primaryType="nt:unstructured"> + <clientlibs + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs" + categories="[coralui3,granite.ui.coral.foundation,acs-commons.contentsync]"/> + </head> + <views jcr:primaryType="nt:unstructured"> + <card + granite:class="contentsync-hosts-task-collection" + jcr:primaryType="nt:unstructured" + jcr:title="Card View" + sling:resourceType="granite/ui/components/coral/foundation/masonry" + icon="viewCard" + layoutId="card" + limit="{Long}20" + selectionCount="single" + selectionMode="{Boolean}false" + size="40" + stateId="shell.collectionpage"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/configure/configurehostdatasource" + itemResourceType="acs-commons/components/utilities/contentsync/configure/configurehostentry" + limit="10" + offset="0" + path="${requestPathInfo.suffix}"/> + <granite:data + jcr:primaryType="nt:unstructured" + foundation-mode-group="contentsync-hosts-task-collection"/> + </card> + </views> + <actions jcr:primaryType="nt:unstructured"> + <primary jcr:primaryType="nt:unstructured"> + <configure + granite:class="foundation-toggleable-control" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + href="#" + icon="gear" + text="General Settings" + variant="actionBar" + x-cq-linkchecker="skip"> + <granite:data + jcr:primaryType="nt:unstructured" + foundation-toggleable-control-action="show" + foundation-toggleable-control-target="#modalGeneralSettings"/> + </configure> + <back + granite:hidden="{Boolean}true" + granite:id="backButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + href="/libs/granite/operations/content/healthreports/healthreportlist.html" + icon="chevronLeft" + variant="actionBar"/> + <add + granite:class="foundation-toggleable-control" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + href="#" + icon="addCircle" + text="Add Host" + variant="actionBar" + x-cq-linkchecker="skip"> + <granite:data + jcr:primaryType="nt:unstructured" + foundation-toggleable-control-action="show" + foundation-toggleable-control-target="#modalConfigureHost"/> + </add> + </primary> + <secondary jcr:primaryType="nt:unstructured"/> + <selection jcr:primaryType="nt:unstructured"> + <configure + granite:hidden="{Boolean}true" + granite:id="configureHealthCheckButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + foundation-collection-action="\{"target":".granite-health-reports-collection", "activeSelectionCount":"single"}" + href="/system/console/configMgr" + icon="gear" + target="_blank" + text="Configure" + variant="actionBar" + x-cq-linkchecker="skip"/> + <view + granite:hidden="{Boolean}true" + granite:id="viewHealthCheckDetailsButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + href="/system/console/configMgr" + icon="browse" + text="View" + variant="actionBar" + x-cq-linkchecker="skip"/> + <delete + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/collection/action" + action="acs-commons.dashboard.task.delete" + activeSelectionCount="single" + href="" + icon="delete" + target=".contentsync-hosts-task-collection" + text="Delete" + variant="actionBar" + x-cq-linkchecker="skip"/> + </selection> + </actions> + <footer + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/container"> + <items jcr:primaryType="nt:unstructured"> + <create + granite:id="modalConfigureHost" + jcr:primaryType="nt:unstructured" + jcr:title="Configure AEM Host To Sync" + sling:resourceType="granite/ui/components/coral/foundation/dialog"> + <items jcr:primaryType="nt:unstructured"> + <form + granite:id="createHostForm" + jcr:primaryType="nt:unstructured" + jcr:title="Configure Host" + sling:resourceType="granite/ui/components/coral/foundation/form" + action="/var/acs-commons/contentsync/hosts/*" + async="{Boolean}true" + enctype="application/x-www-form-urlencoded" + foundationForm="{Boolean}true" + method="POST" + style="vertical"> + <items jcr:primaryType="nt:unstructured"> + <host + granite:id="configureHost-host" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/textfield" + emptyText="http://localhost:4502" + name="./host"/> + <username + granite:id="configureHost-username" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/textfield" + emptyText="username" + name="./username"/> + <password + granite:id="configureHost-password" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/password" + emptyText="password" + name="./password"/> + <resourceType + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/hidden" + ignoreData="{Boolean}true" + name="./sling:resourceType" + value="acs-commons/components/utilities/contentsync/configure/configurehostentry"/> + <primaryType + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/hidden" + name="./jcr:primaryType" + value="nt:unstructured"/> + </items> + <successresponse + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/responses/reload" + target=".cq-workflow-admin-models"/> + </form> + </items> + <footer jcr:primaryType="nt:unstructured"> + <cancel + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + text="Cancel"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </cancel> + <submit + granite:id="createHostFormSubmitBtn" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + formId="createHostForm" + text="Submit" + type="submit" + variant="primary"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </submit> + </footer> + </create> + <general_settings + granite:id="modalGeneralSettings" + jcr:primaryType="nt:unstructured" + jcr:title="General Settings" + sling:resourceType="granite/ui/components/coral/foundation/dialog"> + <items jcr:primaryType="nt:unstructured"> + <form + granite:id="generalSettingsForm" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form" + action="/var/acs-commons/contentsync/settings" + async="{Boolean}true" + dataPath="/var/acs-commons/contentsync/settings" + enctype="application/x-www-form-urlencoded" + foundationForm="{Boolean}true" + method="POST" + style="vertical"> + <items jcr:primaryType="nt:unstructured"> + <event-user-data + granite:id="general-observationData" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/textfield" + fieldDescription="Optional string to pass to the ObservationManager API before changes are committed. This feature can be used to suppress DAM workflows that start when an asset is updated." + fieldLabel="ObservationManager User Data" + name="./event-user-data"/> + <update-strategy + granite:id="general-strategy" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/select" + fieldLabel="Update Strategy" + name="./update-strategy"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/strategydatasource"/> + </update-strategy> + <primaryType + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/hidden" + name="./jcr:primaryType" + value="nt:unstructured"/> + </items> + <successresponse + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/responses/reload" + target=".cq-workflow-admin-models"/> + </form> + </items> + <footer jcr:primaryType="nt:unstructured"> + <cancel + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + text="Cancel"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </cancel> + <submit + granite:id="generalSettingsSubmitBtn" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + formId="generalSettingsForm" + text="Submit" + type="submit" + variant="primary"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </submit> + </footer> + </general_settings> + </items> + </footer> + </jcr:content> +</jcr:root> diff --git a/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml index 1afddf209b..7e5d593eb7 100644 --- a/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml @@ -24,6 +24,7 @@ <generic-lists/> <redirect-manager/> + <contentsync/> <reports/> <exporters/> diff --git a/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml new file mode 100644 index 0000000000..2896fddeb4 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ ACS AEM Commons + ~ + ~ Copyright (C) 2013 - 2023 Adobe + ~ + ~ 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. + --> + +<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" + jcr:primaryType="nt:unstructured" + jcr:title="Content Sync" + jcr:description="Synchronize content between AEM instances" + icon="sync" + href="/apps/acs-commons/content/contentsync.html" + id="acs-commons__contentsync" + target="_blank"/> From ee1179ae71a775745d3b3646d5d2dd73a1fad760 Mon Sep 17 00:00:00 2001 From: david g <davidjgonzalez@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:32:00 -0400 Subject: [PATCH 10/10] Update CHANGELOG.md --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a67eb18e..67055693df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,17 +10,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) ## Unreleased ([details][unreleased changes details]) +## 6.2.0 - 2023-09-14 + +## Added + - #3151 - New ContentSync utility - #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet +- #3166 - New option to suppress status updates in replication workflow processes +## Removed + +- #3183 - Removed .wrap package including JackrabbitSessionIWrap and related classes which is no longer supported in Cloud Manager pipelines. + ## 6.1.0 - 2023-09-08 ## Added - #3159 - Add PageProperty annotation for Sling Models - #3170 - Added a new MCP tool to bulk tag AEM content pages via an Excel file input. -- #3166 - New option to suppress status updates in replication workflow processes - ## Fixed - #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet @@ -29,10 +36,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) - #3150 - Support for case-insensitive redirect rules ( [NC] flag equivalent of apache) - #3138 - Re-arrange action removes data from redirect node -## Removed - -- #3183 - Removed .wrap package including JackrabbitSessionIWrap and related classes which is no longer supported in Cloud Manager pipelines. - ## 6.0.14 - 2023-07-11 ## Fixed