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}