001/*
002 *  Copyright 2017 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.cms.content.referencetable;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashSet;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Optional;
024import java.util.Set;
025
026import org.apache.avalon.framework.activity.Disposable;
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031
032import org.ametys.cms.contenttype.ContentType;
033import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
034import org.ametys.cms.contenttype.MetadataDefinition;
035import org.ametys.cms.repository.Content;
036import org.ametys.cms.repository.ModifiableContent;
037import org.ametys.core.ui.Callable;
038import org.ametys.plugins.repository.AmetysObjectResolver;
039import org.ametys.plugins.repository.metadata.CompositeMetadata;
040import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
041import org.ametys.runtime.plugin.component.AbstractLogEnabled;
042
043import com.google.common.collect.BiMap;
044import com.google.common.collect.HashBiMap;
045
046/**
047 * Helper component for computing information about hierarchy of reference table Contents.
048 * <br><br>
049 * 
050 * At the startup of the Ametys application, you must call {@link #registerRelation(ContentType, ContentType)} in order to register a <b>relation</b> between a <b>parent</b> content type and its <b>child</b> content type.<br>
051 * When all relations are registered, one or several <b>hierarchy(ies)</b> can be inferred, following some basic rules:<br>
052 * <ul>
053 * <li>A hierarchy of two or more content types cannot be cyclic</li>
054 * <li>A content type can have itself as its parent content type</li>
055 * <li>A content type cannot have two different parent content types</li>
056 * <li>A content type can have only one content type as children, plus possibly itself</li>
057 * </ul>
058 * From each hierarchy of content types, a <b>tree</b> of contents can be inferred.<br>
059 * <br>
060 * For instance, the following examples of hierarchy are valid (where <b>X←Y</b> means 'X is the parent content type of Y'; <br>and <b>⤹Z</b> means 'Z is the parent content type of Z'):
061 * <ul>
062 * <li>B←A</li>
063 * <li>E←D←C←B←A</li>
064 * <li>⤹B←A (content type B defines two different child content types, but one is itself, which is allowed)</li>
065 * <li>⤹E←D←C←B←A</li>
066 * <li>⤹A</li>
067 * </ul>
068 * ; and the following examples of hierarchy are invalid:
069 * <ul>
070 * <li>C←B and C←A (a content type cannot have multiple content types as children, which are not itself)</li>
071 * <li>C←A and B←A (a content type cannot have two different parent content types)</li>
072 * <li>⤹A and B←A (a content type cannot have two different parent content types, even if one is itself)</li>
073 * <li>A←B and B←A (cyclic hierarchy)</li>
074 * <li>A←C←B←A (cyclic hierarchy)</li>
075 * </ul>
076 */
077public class HierarchicalReferenceTablesHelper extends AbstractLogEnabled implements Component, Serviceable, Disposable
078{
079    /** The Avalon role */
080    public static final String ROLE = HierarchicalReferenceTablesHelper.class.getName();
081    
082    /** The extension point for content types */
083    protected ContentTypeExtensionPoint _contentTypeEP;
084    /** The Ametys objet resolver */
085    protected AmetysObjectResolver _resolver;
086    
087    /** The map parent -&gt; child (excepted content types pointing on themselves) */
088    private BiMap<ContentType, ContentType> _childByContentType = HashBiMap.create();
089    /** The content types pointing at themselves */
090    private Set<ContentType> _autoReferencingContentTypes = new HashSet<>();
091    /** The map leafContentType -&gt; topLevelContentType  */
092    private BiMap<ContentType, ContentType> _topLevelTypeByLeafType = HashBiMap.create();
093
094    @Override
095    public void service(ServiceManager manager) throws ServiceException
096    {
097        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
098        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
099    }
100    
101    @Override
102    public void dispose()
103    {
104        _childByContentType.clear();
105        _autoReferencingContentTypes.clear();
106        _topLevelTypeByLeafType.clear();
107    }
108    
109    /**
110     * Register a relation between a parent and its child, and update the internal model if it is a valid one.
111     * @param parent The parent content type
112     * @param child The child content type
113     * @return true if the relation is valid, i.e. in accordance with what was registered before
114     */
115    public boolean registerRelation(ContentType parent, ContentType child)
116    {
117        if (parent.equals(child))
118        {
119            _autoReferencingContentTypes.add(parent);
120            if (_childByContentType.containsKey(parent))
121            {
122                // _topLevelTypeByLeafType does not need to be updated as another content type references it
123            }
124            else
125            {
126                // _topLevelTypeByLeafType needs to be updated as no other content type references it
127                _topLevelTypeByLeafType.put(parent, parent);
128            }
129            return true;
130        }
131        else if (_childByContentType.containsKey(parent))
132        {
133            getLogger().error("Problem of definition of parent for content type '{}'. Its parent '{}' is already declared by '{}'. A content type cannot have multiple content types as children", child, parent, _childByContentType.get(parent));
134            return false;
135        }
136        else if (_checkNoCycle(parent, child))
137        {
138            // ok valid
139            // update _childByContentType
140            _childByContentType.put(parent, child);
141            
142            // update _topLevelTypeByLeafType
143            boolean containsParentAsKey = _topLevelTypeByLeafType.containsKey(parent);
144            boolean containsChildAsValue = _topLevelTypeByLeafType.containsValue(child);
145            if (containsParentAsKey && containsChildAsValue)
146            {
147                // is currently something as {parent: other, other2: child}, which means the hierarchy is like: other -> parent -> child -> other2
148                // we now want {other2: other}
149                ContentType other = _topLevelTypeByLeafType.remove(parent);
150                ContentType other2 = _topLevelTypeByLeafType.inverse().get(child);
151                _topLevelTypeByLeafType.put(other2, other);
152            }
153            else if (containsParentAsKey)
154            {
155                // is currently something as {parent: other}, which means the hierarchy is like: other -> ... -> parent -> child
156                // we now want {child: other}
157                ContentType other = _topLevelTypeByLeafType.remove(parent);
158                _topLevelTypeByLeafType.put(child, other);
159            }
160            else if (containsChildAsValue)
161            {
162                // is currently something as {other: child}, which means the hierarchy is like: parent -> child -> ... -> other
163                // we now want {other: parent}
164                ContentType other = _topLevelTypeByLeafType.inverse().get(child);
165                _topLevelTypeByLeafType.put(other, parent);
166            }
167            else
168            {
169                _topLevelTypeByLeafType.put(child, parent);
170            }
171            return true;
172        }
173        else
174        {
175            // An error was logged in #_checkNoCycle method
176            return false;
177        }
178    }
179    
180    /**
181     * Returns true if at least one hierarchy was registered (i.e. at least one content type defines a valid "parent" metadata)
182     * @return true if at least one hierarchy was registered
183     */
184    public boolean hasAtLeastOneHierarchy()
185    {
186        return !_childByContentType.isEmpty() || !_autoReferencingContentTypes.isEmpty();
187    }
188    
189    /**
190     * Gets the top level content type for the given leaf content type (which defines the hierarchy)
191     * @param leafContentType the leaf cotnent type
192     * @return the top level content type for the given leaf content type
193     */
194    public ContentType getTopLevelType(ContentType leafContentType)
195    {
196        return _topLevelTypeByLeafType.get(leafContentType);
197    }
198    
199    private boolean _checkNoCycle(ContentType parent, ContentType child)
200    {
201        // at this stage, parent is not equal to child
202        
203        Set<ContentType> contentTypesInHierarchy = new HashSet<>();
204        contentTypesInHierarchy.add(child);
205        contentTypesInHierarchy.add(parent);
206        
207        ContentType parentContentType = parent;
208        
209        do
210        {
211            final ContentType currentContentType = parentContentType;
212            parentContentType = Optional.ofNullable(currentContentType)
213                                        .map(ContentType::getParentMetadata)
214                                        .map(MetadataDefinition::getContentType)
215                                        .map(_contentTypeEP::getExtension)
216                                        .filter(par -> !currentContentType.equals(par)) // if current equals to parent, set to null as there will be no cycle
217                                        .orElse(null);
218            if (contentTypesInHierarchy.contains(parentContentType))
219            {
220                // there is a cycle, log an error and return false
221                getLogger().error("Problem of definition of parent for content type {}. It defines a cyclic hierarchy (The following content types are involved: {}).", child, contentTypesInHierarchy);
222                return false;
223            }
224            contentTypesInHierarchy.add(parentContentType);
225        }
226        while (parentContentType != null);
227        
228        // no cycle, it is ok, return true
229        return true;
230    }
231    
232    /**
233     * Returns true if the given content type has a child content type
234     * @param contentType The content type
235     * @return true if the given content type has a child content type
236     */
237    public boolean hasChildContentType(ContentType contentType)
238    {
239        return _childByContentType.containsKey(contentType) || _autoReferencingContentTypes.contains(contentType);
240    }
241    
242    /**
243     * Returs true if the given content type is hierarchical, i.e. is part of a hierarchy
244     * @param contentType The content type
245     * @return true if the given content type is hierarchical, i.e. is part of a hierarchy
246     */
247    public boolean isHierarchical(ContentType contentType)
248    {
249        return _childByContentType.containsKey(contentType) || _childByContentType.containsValue(contentType) || _autoReferencingContentTypes.contains(contentType);
250    }
251    
252    /**
253     * Returns true if the given content type is a leaf content type
254     * @param contentType The content type
255     * @return true if the given content type is a leaf content type
256     */
257    public boolean isLeaf(ContentType contentType)
258    {
259        return _topLevelTypeByLeafType.containsKey(contentType);
260    }
261    
262    /**
263     * Get the hierarchy of content types (distinct content types) 
264     * @param leafContentTypeId The id of leaf content type
265     * @return The content types of hierarchy
266     */
267    public Set<String> getHierarchicalContentTypes(String leafContentTypeId)
268    {
269        Set<String> hierarchicalTypes = new LinkedHashSet<>();
270        hierarchicalTypes.add(leafContentTypeId);
271        BiMap<ContentType, ContentType> parentByContentType = _childByContentType.inverse();
272     
273        ContentType leafContentType = _contentTypeEP.getExtension(leafContentTypeId);
274        
275        ContentType parentContentType = parentByContentType.get(leafContentType);
276        while (parentContentType != null)
277        {
278            hierarchicalTypes.add(parentContentType.getId());
279            parentContentType = parentByContentType.get(parentContentType);
280        }
281        return hierarchicalTypes;
282    }
283    
284    /**
285     * Get the path of reference table entry in its hierarchy
286     * @param refTableEntryId The id of entry
287     * @return The path from root parent
288     */
289    @Callable
290    public String getPathInHierarchy(String refTableEntryId)
291    {
292        Content refTableEntry = _resolver.resolveById(refTableEntryId);
293        List<String> paths = new ArrayList<>();
294        paths.add(refTableEntry.getName());
295        
296        String parentId = getParent(refTableEntry);
297        while (parentId != null)
298        {
299            Content parent = _resolver.resolveById(parentId);
300            paths.add(parent.getName());
301            parentId = getParent(parent);
302        }
303        
304        Collections.reverse(paths);
305        return org.apache.commons.lang3.StringUtils.join(paths, "/");
306    }
307    
308    /**
309     * Gets the content types the children of the given content can have.
310     * The result can contain 0, 1 or 2 content types
311     * @param refTableEntry The content
312     * @return the content types the children of the given content can have.
313     */
314    public List<ContentType> getChildContentTypes(Content refTableEntry)
315    {
316        List<ContentType> result = new ArrayList<>();
317        
318        for (String cTypeId : refTableEntry.getTypes())
319        {
320            ContentType cType = _contentTypeEP.getExtension(cTypeId);
321            if (_childByContentType.containsKey(cType))
322            {
323                result.add(_childByContentType.get(cType));
324            }
325            if (_autoReferencingContentTypes.contains(cType))
326            {
327                result.add(cType);
328            }
329            
330            if (!result.isEmpty())
331            {
332                break;
333            }
334        }
335        
336        return result;
337    }
338    
339    /**
340     * Sets the "parent" metadata of the given content
341     * See also {@link ContentType#getParentMetadata()}
342     * @param content The content
343     * @param parent The value of the "parent" metadata
344     */
345    public void setParentMetadata(ModifiableContent content, Content parent)
346    {
347        for (String cTypeId : content.getTypes())
348        {
349            ContentType cType = _contentTypeEP.getExtension(cTypeId);
350            MetadataDefinition parentMetadata = cType.getParentMetadata();
351            if (parentMetadata != null)
352            {
353                _setMetadata(parentMetadata, content, parent);
354                break;
355            }
356        }
357    }
358    
359    private void _setMetadata(MetadataDefinition metadataDefinition, ModifiableContent content, Content value)
360    {
361        ModifiableCompositeMetadata metadataHolder = content.getMetadataHolder();
362        String metadataName = metadataDefinition.getName();
363        
364        // Set metadata
365        metadataHolder.setMetadata(metadataName, value.getId());
366    }
367    
368    /**
369     * Returns the "parent" metadata value for the given content, or null if it is not defined for its content types
370     * See also {@link ContentType#getParentMetadata()}
371     * @param content The content
372     * @return the "parent" metadata value for the given content, or null
373     */
374    public String getParent(Content content)
375    {
376        for (String cTypeId : content.getTypes())
377        {
378            if (!_contentTypeEP.hasExtension(cTypeId))
379            {
380                getLogger().warn("The content {} ({}) has unknown type '{}'", content.getId(), content.getTitle(), cTypeId);
381                continue;
382            }
383            ContentType cType = _contentTypeEP.getExtension(cTypeId);
384            MetadataDefinition parentMetadata = cType.getParentMetadata();
385            if (parentMetadata != null)
386            {
387                CompositeMetadata metadataHolder = content.getMetadataHolder();
388                String metadataName = parentMetadata.getName();
389                if (metadataHolder.hasMetadata(metadataName))
390                {
391                    return content.getMetadataHolder().getString(metadataName);
392                }
393            }
394        }
395        return null;
396    }
397    
398    /**
399     * Recursively returns the "parent" metadata value for the given content, i.e; will return the parent, the parent of the parent, etc.
400     * @param content The content
401     * @return all the parents of the given content
402     */
403    public List<String> getAllParents(Content content)
404    {
405        List<String> parents = new ArrayList<>();
406        
407        Content currentContent = content;
408        String parentId = getParent(currentContent);
409        
410        while (parentId != null)
411        {
412            if (_resolver.hasAmetysObjectForId(parentId))
413            {
414                parents.add(parentId);
415                currentContent = _resolver.resolveById(parentId);
416                parentId = getParent(currentContent);
417            }
418            else
419            {
420                break;
421            }
422        }
423        
424        return parents;
425    }
426}