001/*
002 *  Copyright 2014 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 */
016
017package org.ametys.cms.content.references;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.HashMap;
022import java.util.Map;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.logger.AbstractLogEnabled;
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.lang3.StringUtils;
030
031import org.ametys.cms.contenttype.ContentConstants;
032import org.ametys.cms.contenttype.ContentTypesHelper;
033import org.ametys.cms.contenttype.MetadataDefinition;
034import org.ametys.cms.contenttype.MetadataType;
035import org.ametys.cms.contenttype.RepeaterDefinition;
036import org.ametys.cms.repository.Content;
037import org.ametys.plugins.repository.metadata.CompositeMetadata;
038import org.ametys.plugins.repository.metadata.RichText;
039
040/**
041 * The outgoing references extractor is a class that analyzes a content to extract the list of outgoing references in the metadata.
042 * Theses references should be stored on the content to allow research or post-processing later (example: consistency analyzer).
043 */
044public class OutgoingReferencesExtractor extends AbstractLogEnabled implements Serviceable, Component
045{
046    /** Avalon role */
047    public static final String ROLE = OutgoingReferencesExtractor.class.getName();
048    
049    /** Constants for the explorer type for outgoing references */
050    private static final String __OUTGOING_REFERENCE_TYPE_EXPLORER = "explorer";
051
052    /** Helper for content types */
053    protected ContentTypesHelper _contentTypesHelper;
054    
055    @Override
056    public void service(ServiceManager serviceManager) throws ServiceException
057    {
058        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
059    }
060    
061    /**
062     * This method analyzes the content to return the list of outgoing references grouped by metadata path.
063     * @param content The content to analyze
064     * @return A map where keys are metadata path and values are outgoing references for this metadata. Can not be null.
065     */
066    public Map<String, OutgoingReferences> getOutgoingReferences(Content content)
067    {
068        return _analyzeCompositeMetadata(content, null, content.getMetadataHolder(), StringUtils.EMPTY);
069    }
070    
071    /**
072     * Analyze a composite metadata to extract the outgoing references
073     * @param content The current content
074     * @param parentMetadataDefinition The parent metadata definition (can be null, at the root level)
075     * @param metadataHolder The composite metadata to analyze. Cannot be null
076     * @param metadataPathPrefix The path of the current metadata holder
077     * @return The outgoing references, indexed by metadata path.
078     */
079    protected Map<String, OutgoingReferences> _analyzeCompositeMetadata(Content content, MetadataDefinition parentMetadataDefinition, CompositeMetadata metadataHolder, String metadataPathPrefix)
080    {
081        Map<String, OutgoingReferences> outgoingReferencesByPath = new HashMap<>();
082        
083        for (String metadataName : metadataHolder.getMetadataNames())
084        {
085            MetadataDefinition metadataDefinition = _getMetadataDefinition(content, parentMetadataDefinition, metadataName);
086            if (metadataDefinition != null) // Filter out metadata not declared in content type's definition
087            {
088                OutgoingReferences outgoingReferences = null;
089                
090                String metadataPath = metadataPathPrefix + metadataName;
091                MetadataType type = metadataDefinition.getType();
092                
093                switch (type)
094                {
095                    case COMPOSITE:
096                        if (metadataDefinition instanceof RepeaterDefinition)
097                        {
098                            CompositeMetadata repeaterHolder = metadataHolder.getCompositeMetadata(metadataName);
099                            for (String entry : repeaterHolder.getMetadataNames())
100                            {
101                                CompositeMetadata entryMetadataHolder = repeaterHolder.getCompositeMetadata(entry);
102                                
103                                Map<String, OutgoingReferences> entryOutgoingReferences = _analyzeCompositeMetadata(content, metadataDefinition, entryMetadataHolder, metadataPath + '[' + entry + ']' + ContentConstants.METADATA_PATH_SEPARATOR);
104                                outgoingReferencesByPath.putAll(entryOutgoingReferences);
105                            }
106                        }
107                        else
108                        {
109                            Map<String, OutgoingReferences> compOutgoingReferences = _analyzeCompositeMetadata(content, metadataDefinition, metadataHolder.getCompositeMetadata(metadataName), metadataPath + ContentConstants.METADATA_PATH_SEPARATOR);
110                            outgoingReferencesByPath.putAll(compOutgoingReferences); 
111                        }
112                        break;
113                    case FILE:
114                        // Could be a binary or a string.
115                        if (!CompositeMetadata.MetadataType.BINARY.equals(metadataHolder.getType(metadataName)))
116                        {
117                            outgoingReferences = _analyzeResourceFileMetadata(metadataDefinition, metadataHolder.getString(metadataName));
118                        }
119                        break;
120                    case REFERENCE:
121                        CompositeMetadata compositeReference = metadataHolder.getCompositeMetadata(metadataName);
122                        outgoingReferences = _analyzeReferenceMetadata(metadataDefinition, compositeReference);
123                        break;
124                    case RICH_TEXT:
125                        outgoingReferences = _analyzeMetadata(metadataDefinition, metadataHolder.getRichText(metadataName)); 
126                        break;
127                    case BOOLEAN:
128                    case LONG:
129                    case DOUBLE:
130                    case DATE:
131                    case DATETIME:
132                    case GEOCODE:
133                    case STRING:
134                    case USER:
135                    case CONTENT:
136                    case SUB_CONTENT:
137                    case BINARY:
138                        break;
139                    default:
140                        getLogger().warn(
141                                "The outgoing references extractor does not handle metadata of type '" + type + "'.\nSkipping extraction process for the metadata '"
142                                        + metadataDefinition.getId() + "'.");
143                        break;
144                }
145                
146                // Update outgoing references by path map.
147                if (outgoingReferences != null)
148                {
149                    outgoingReferencesByPath.put(metadataPath, outgoingReferences);
150                }
151            }
152        }
153        
154        return outgoingReferencesByPath;
155    }
156    
157    /**
158     * Analyze a rich text metadata.
159     * Delegates processing to the rich text outgoing references extractor of the metadata definition 
160     * @param metadataDefinition The metadata definition corresponding to the current metadata
161     * @param metadata The metadata to analyze. Cannot be null
162     * @return The corresponding outgoing references.
163     */
164    protected OutgoingReferences _analyzeMetadata(MetadataDefinition metadataDefinition, RichText metadata)
165    {
166        return metadataDefinition.getRichTextOutgoingReferencesExtractor().getOutgoingReferences(metadata);
167    }
168    
169    /**
170     * Analyze a string file metadata.
171     * @param metadataDefinition The metadata definition corresponding to the current metadata
172     * @param metadata The metadata to analyze. Cannot be null
173     * @return The corresponding outgoing references.
174     */
175    protected OutgoingReferences _analyzeResourceFileMetadata(MetadataDefinition metadataDefinition, String metadata)
176    {
177        if (StringUtils.isNotEmpty(metadata))
178        {
179            OutgoingReferences references = new OutgoingReferences();
180            
181            ArrayList<String> referenceValues = new ArrayList<>(1);
182            referenceValues.add(metadata);
183            
184            references.put(__OUTGOING_REFERENCE_TYPE_EXPLORER, referenceValues);
185            
186            return references;
187        }
188        
189        return null;
190    }
191    
192    /**
193     * Analyze a reference metadata.
194     * @param metadataDefinition The metadata definition corresponding to the current metadata
195     * @param metadata The reference metadata
196     * @return The corresponding outgoing references.
197     */
198    protected OutgoingReferences _analyzeReferenceMetadata(MetadataDefinition metadataDefinition, CompositeMetadata metadata)
199    {
200        OutgoingReferences references = new OutgoingReferences();
201        
202        String value = metadata.getString("value");
203        
204        references.put(metadata.getString("type"), Arrays.asList(new String[] {value}));
205        return references;
206    }
207    
208    /**
209     * Retrieves a sub metadata definition from a content type or
210     * a parent metadata definition. 
211     * @param content the content.
212     * @param parentMetadataDefinition the parent metadata definition.
213     * @param metadataName the metadata name.
214     * @return the metadata definition found or <code>null</code> otherwise.
215     */
216    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition parentMetadataDefinition, String metadataName)
217    {
218        if (parentMetadataDefinition == null)
219        {
220            return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
221        }
222        else
223        {
224            return parentMetadataDefinition.getMetadataDefinition(metadataName);
225        }
226    }
227}