001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.odfsync.cdmfr; 017 018import java.util.Arrays; 019import java.util.HashSet; 020import java.util.Set; 021 022import javax.jcr.NodeIterator; 023import javax.jcr.RepositoryException; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang.StringUtils; 030import org.apache.excalibur.xml.xpath.XPathProcessor; 031import org.slf4j.Logger; 032import org.w3c.dom.Node; 033 034import org.ametys.cms.content.external.ExternalizableMetadataHelper; 035import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus; 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.cms.contenttype.MetadataDefinition; 039import org.ametys.cms.contenttype.MetadataType; 040import org.ametys.cms.contenttype.RepeaterDefinition; 041import org.ametys.odf.program.SubProgram; 042import org.ametys.odf.program.SubProgramFactory; 043import org.ametys.plugins.repository.AmetysObjectResolver; 044import org.ametys.plugins.repository.RepositoryConstants; 045import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 046import org.ametys.plugins.repository.metadata.jcr.JCRCompositeMetadata; 047 048/** 049 * Helper for synchronizing and merging metadata from shared programs 050 */ 051public class MergeMetadataForSharedProgramHelper implements Serviceable, Component 052{ 053 /** The avalon role */ 054 public static final String ROLE = MergeMetadataForSharedProgramHelper.class.getName(); 055 056 /** The name of the JCR node holding the shared metadata */ 057 public static final String SHARED_PROGRAMS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":shared-programs"; 058 059 /** The xpath processor */ 060 protected static XPathProcessor _xPathProcessor; 061 062 /** The content type extension point */ 063 protected ContentTypeExtensionPoint _cTypeE; 064 065 /** The ametys object resolver */ 066 protected AmetysObjectResolver _resolver; 067 068 069 @Override 070 public void service(ServiceManager manager) throws ServiceException 071 { 072 _cTypeE = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 073 _xPathProcessor = (XPathProcessor) manager.lookup(XPathProcessor.ROLE); 074 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 075 } 076 077 /** 078 * Merge the shared metadata 079 * @param mainProgram the main program 080 * @param contentNode The DOM representing the content 081 * @param metadataToMerge the metadata to merge 082 * @param logger the logger 083 * @return true if there are some changes 084 * @throws RepositoryException if an error occurred 085 */ 086 public boolean mergeSharedMetadata(SubProgram mainProgram, Node contentNode, Set<String> metadataToMerge, Logger logger) throws RepositoryException 087 { 088 boolean hasChanged = false; 089 090 for (String metadataName : metadataToMerge) 091 { 092 ContentType cType = _cTypeE.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE); 093 094 if (isValidMetadataPath(cType, metadataName, logger)) 095 { 096 MetadataDefinition metadataDefinition = cType.getMetadataDefinition(metadataName); 097 if (metadataDefinition.isMultiple()) 098 { 099 hasChanged = mergeSharedMultipleMetadata(metadataDefinition, mainProgram, contentNode, metadataName, logger) || hasChanged; 100 } 101 else 102 { 103 hasChanged = mergeSharedSingleMetadata(metadataDefinition, mainProgram, metadataName, logger) || hasChanged; 104 } 105 } 106 } 107 108 return hasChanged; 109 } 110 111 /** 112 * Synchronize multiple metadata for main program 113 * @param metadataDefinition the metadata definition 114 * @param mainSubProgram the main subprogram 115 * @param contentNode The DOM representing the content 116 * @param metadataName the metadata name 117 * @param logger the logger 118 * @return true if there are some changes 119 * @throws RepositoryException if an error occurred 120 */ 121 protected boolean mergeSharedMultipleMetadata(MetadataDefinition metadataDefinition, SubProgram mainSubProgram, Node contentNode, String metadataName, Logger logger) throws RepositoryException 122 { 123 MetadataType type = metadataDefinition.getType(); 124 switch (type) 125 { 126 case STRING: 127 case REFERENCE: 128 case CONTENT: 129 // Merge the values of all shared subprograms 130 String[] mergeValues = getMergeSharedStringArrayMetadata(mainSubProgram, metadataName, logger); 131 132 ModifiableCompositeMetadata metadataHolder = mainSubProgram.getMetadataHolder(); 133 ExternalizableMetadataStatus status = ExternalizableMetadataHelper.getStatus(metadataHolder, metadataName); 134 if (status == ExternalizableMetadataStatus.EXTERNAL) 135 { 136 return ExternalizableMetadataHelper.setExternalMetadata(metadataHolder, metadataName, mergeValues, true); 137 } 138 else 139 { 140 return ExternalizableMetadataHelper.setMetadata(metadataHolder, metadataName, mergeValues); 141 } 142 default: 143 String warnMessage = "La fusion des métadonnées n'est pas gérée pour le type '" + type.toString() + "'"; 144 logger.warn(warnMessage); 145 return false; 146 } 147 } 148 149 /** 150 * Synchronize single metadata for main program 151 * @param metadataDefinition the metadata definition 152 * @param mainProgram the main program 153 * @param metadataPath the metadata path 154 * @param logger the logger 155 * @return true if there are some changes 156 * @throws RepositoryException if an error occurred 157 */ 158 protected boolean mergeSharedSingleMetadata(MetadataDefinition metadataDefinition, SubProgram mainProgram, String metadataPath, Logger logger) throws RepositoryException 159 { 160 MetadataType type = metadataDefinition.getType(); 161 switch (type) 162 { 163 case COMPOSITE: 164 if (metadataDefinition instanceof RepeaterDefinition) 165 { 166 return mergeSharedRepeaterMetadata(metadataDefinition, mainProgram, metadataPath, logger); 167 } 168 return false; 169 default: 170 String warnMessage = "La fusion des métadonnées n'est pas gérée pour le type '" + type.toString() + "'"; 171 logger.warn(warnMessage); 172 return false; 173 } 174 } 175 176 /** 177 * Get and merge a String array shared metadata 178 * @param mainSubProgram The main subprogram 179 * @param metadataName The metadata name 180 * @param logger the logger 181 * @return the merge metadata in a String array 182 * @throws RepositoryException if an error occurred 183 */ 184 protected String[] getMergeSharedStringArrayMetadata (SubProgram mainSubProgram, String metadataName, Logger logger) throws RepositoryException 185 { 186 Set<String> values = new HashSet<>(); 187 188 javax.jcr.Node sharedProgramRootNode = getSharedProgramRootNode(mainSubProgram, logger); 189 NodeIterator nodes = sharedProgramRootNode.getNodes(); 190 191 while (nodes.hasNext()) 192 { 193 javax.jcr.Node sharedProgramNode = (javax.jcr.Node) nodes.next(); 194 String code = sharedProgramNode.getName(); 195 ModifiableCompositeMetadata sharedMetaHolder = getSharedMetadataHolder(mainSubProgram, code, logger); 196 197 String[] sharedValues = sharedMetaHolder.getStringArray(metadataName, new String[0]); 198 values.addAll(Arrays.asList(sharedValues)); 199 } 200 201 return values.toArray(new String[values.size()]); 202 } 203 204 /** 205 * Merge all repeater entry to the main program 206 * @param metadataDefinition the metadata definition 207 * @param mainSubProgram the main subprogram 208 * @param metadataName the metadata name 209 * @param logger the logger 210 * @return true if there are some changes 211 * @throws RepositoryException if an error occurred 212 */ 213 protected boolean mergeSharedRepeaterMetadata(MetadataDefinition metadataDefinition, SubProgram mainSubProgram, String metadataName, Logger logger) throws RepositoryException 214 { 215 boolean hasChanged = false; 216 217 // First removed old entries if exist 218 if (mainSubProgram.getMetadataHolder().hasMetadata(metadataName)) 219 { 220 mainSubProgram.getMetadataHolder().removeMetadata(metadataName); 221 hasChanged = true; 222 } 223 224 ModifiableCompositeMetadata mergedRepeater = null; 225 int index = 1; 226 227 javax.jcr.Node sharedProgramRootNode = getSharedProgramRootNode(mainSubProgram, logger); 228 NodeIterator nodes = sharedProgramRootNode.getNodes(); 229 230 while (nodes.hasNext()) 231 { 232 javax.jcr.Node sharedProgramNode = (javax.jcr.Node) nodes.next(); 233 String code = sharedProgramNode.getName(); 234 ModifiableCompositeMetadata sharedMetaHolder = getSharedMetadataHolder(mainSubProgram, code, logger); 235 236 if (sharedMetaHolder.hasMetadata(metadataName)) 237 { 238 ModifiableCompositeMetadata repeaterMetadata = sharedMetaHolder.getCompositeMetadata(metadataName); 239 240 // Iterate over entries 241 String[] entryNames = repeaterMetadata.getMetadataNames(); 242 for (String entryName : entryNames) 243 { 244 ModifiableCompositeMetadata entry = repeaterMetadata.getCompositeMetadata(entryName); 245 246 if (mergedRepeater == null) 247 { 248 mergedRepeater = mainSubProgram.getMetadataHolder().getCompositeMetadata(metadataName, true); 249 } 250 251 ModifiableCompositeMetadata newEntry = mergedRepeater.getCompositeMetadata(String.valueOf(index), true); 252 entry.copyTo(newEntry); 253 254 index++; 255 hasChanged = true; 256 } 257 } 258 } 259 260 return hasChanged; 261 } 262 263 /** 264 * Get the shared program node holding the shared subprogram node 265 * @param mainProgram the main program 266 * @param logger the logger 267 * @return root node 268 * @throws RepositoryException if an error occurred 269 */ 270 public javax.jcr.Node getSharedProgramRootNode(SubProgram mainProgram, Logger logger) throws RepositoryException 271 { 272 javax.jcr.Node node = mainProgram.getNode(); 273 274 javax.jcr.Node rootNode = null; 275 if (node.hasNode(SHARED_PROGRAMS_NODE_NAME)) 276 { 277 rootNode = node.getNode(SHARED_PROGRAMS_NODE_NAME); 278 } 279 else 280 { 281 rootNode = node.addNode(SHARED_PROGRAMS_NODE_NAME, "ametys:compositeMetadata"); 282 } 283 284 return rootNode; 285 } 286 287 /** 288 * Get the composite metadata holding the shared metadata for subprogram of given code 289 * @param mainProgram the main program 290 * @param subProgramCode the sub program code 291 * @param logger the logger 292 * @return the shared composite metadata 293 * @throws RepositoryException if an error occurred 294 */ 295 public ModifiableCompositeMetadata getSharedMetadataHolder(SubProgram mainProgram, String subProgramCode, Logger logger) throws RepositoryException 296 { 297 javax.jcr.Node rootNode = getSharedProgramRootNode(mainProgram, logger); 298 299 javax.jcr.Node node = null; 300 if (rootNode.hasNode(subProgramCode)) 301 { 302 node = rootNode.getNode(subProgramCode); 303 } 304 else 305 { 306 node = rootNode.addNode(subProgramCode, "ametys:compositeMetadata"); 307 } 308 309 return new JCRCompositeMetadata(node, _resolver); 310 } 311 312 /** 313 * Determines if the metadata path is a valid path from the given content type and eligible to merge 314 * @param cType The content type 315 * @param metadataPath The metadata path 316 * @param logger the logger 317 * @return true if the metadata can be merged 318 */ 319 public boolean isValidMetadataPath (ContentType cType, String metadataPath, Logger logger) 320 { 321 if (cType.getMetadataDefinition(metadataPath) == null) 322 { 323 String warn = "La métadonnée '" + metadataPath + "' n'existe pas. Elle sera ignorée lors de la fusion de parcours partagés."; 324 logger.warn(warn); 325 326 return false; 327 } 328 329 if (StringUtils.contains(metadataPath, "/")) 330 { 331 String warn = "Les métadonnées de type 'composite' ne sont pas supportées pour la fusion de parcours partagés: la métadonnée '" + metadataPath + " sera ignorée."; 332 logger.warn(warn); 333 334 return false; 335 } 336 337 return true; 338 } 339}