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