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.plugins.contentio.synchronize;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import javax.jcr.RepositoryException;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036
037import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
038import org.ametys.cms.repository.Content;
039import org.ametys.plugins.repository.AmetysRepositoryException;
040import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
041import org.ametys.plugins.repository.query.expression.Expression;
042import org.ametys.plugins.repository.query.expression.Expression.Operator;
043import org.ametys.plugins.repository.query.expression.ExpressionContext;
044import org.ametys.plugins.repository.query.expression.StringExpression;
045import org.ametys.runtime.plugin.component.AbstractLogEnabled;
046
047/**
048 * Helper for Synchronizable Contents Collections.
049 */
050public class SynchronizableContentsCollectionHelper extends AbstractLogEnabled implements Serviceable, Component
051{
052    /** The Avalon Role */
053    public static final String ROLE = SynchronizableContentsCollectionHelper.class.getName();
054    
055    /** SCC DAO */
056    protected SynchronizableContentsCollectionDAO _sccDAO;
057    /** The content type extension point */
058    protected ContentTypeExtensionPoint _contentTypeEP;
059    
060    @Override
061    public void service(ServiceManager smanager) throws ServiceException
062    {
063        _sccDAO = (SynchronizableContentsCollectionDAO) smanager.lookup(SynchronizableContentsCollectionDAO.ROLE);
064        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
065    }
066    
067    /**
068     * Get the first {@link SynchronizableContentsCollection} found for the given SCC model id.
069     * @param modelId Id of the SCC model
070     * @return The first SCC found or null
071     */
072    public SynchronizableContentsCollection getSCCFromModelId(String modelId)
073    {
074        SynchronizableContentsCollection collection = null;
075
076        // Get the first collection corresponding to the SCC model
077        for (SynchronizableContentsCollection scc : _sccDAO.getSynchronizableContentsCollections())
078        {
079            if (scc.getSynchronizeCollectionModelId().equals(modelId))
080            {
081                collection = scc;
082                break;
083            }
084        }
085        
086        return collection;
087    }
088
089    /**
090     * Transform results to be organized by content attribute, and remove the null values.
091     * @param searchResult Remote values from a search by content and column or attribute
092     * @param mapping Mapping between content attribute and columns/attributes
093     * @return A {@link Map} of possible attribute values organized by content synchronization key and attribute name
094     */
095    public Map<String, Map<String, List<Object>>> organizeRemoteValuesByAttribute(Map<String, Map<String, Object>> searchResult, Map<String, List<String>> mapping)
096    {
097        Map<String, Map<String, List<Object>>> result = new LinkedHashMap<>();
098        
099        // For each searchResult line (1 line = 1 content)
100        for (String resultKey : searchResult.keySet())
101        {
102            Map<String, Object> searchItem = searchResult.get(resultKey);
103            Map<String, List<Object>> contentResult = new HashMap<>();
104            
105            // For each attribute in the mapping
106            for (String attributeName : mapping.keySet())
107            {
108                List<String> columns = mapping.get(attributeName); // Get the columns for the current attribute
109                List<Object> values = columns.stream() // For each column corresponding to the attribute
110                        .map(searchItem::get) // Map the values
111                        .flatMap(o ->
112                        {
113                            if (o instanceof Collection<?>)
114                            {
115                                return ((Collection<?>) o).stream();
116                            }
117                            return Stream.of(o);
118                        }) // If it's a list of objects, get a flat stream
119                        .filter(Objects::nonNull) // Remove null values
120                        .collect(Collectors.toList()); // Collect it into a List
121                
122                contentResult.put(attributeName, values); // Add the retrieved attribute values list to the contentResult
123            }
124            
125            result.put(resultKey, contentResult);
126        }
127        
128        return result;
129    }
130    
131    /**
132     * Add the given synchronizable collection id to the existing ones
133     * @param content The synchronized content
134     * @param collectionId The ID of the collection to add
135     * @throws RepositoryException if an error occurred
136     */
137    public void updateSCCProperty(Content content, String collectionId) throws RepositoryException
138    {
139        Set<String> collectionIds = getSynchronizableCollectionIds(content);
140        collectionIds.add(collectionId);
141
142        content.getInternalDataHolder().setValue(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME, collectionIds.toArray(new String[collectionIds.size()]));
143    }
144    
145    /**
146     * Retrieves the synchronizable collection identifiers
147     * @param content the content
148     * @return the synchronizable collection identifiers
149     * @throws AmetysRepositoryException if an error occurs while reading SCC info on the given content
150     */
151    public Set<String> getSynchronizableCollectionIds(Content content) throws AmetysRepositoryException
152    {
153        ModelLessDataHolder internalDataHolder = content.getInternalDataHolder();
154        
155        Set<String> collectionIds = new HashSet<>();
156        if (internalDataHolder.hasValue(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME))
157        {
158            String[] existingCollectionIds = internalDataHolder.getValue(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME);
159            collectionIds = Arrays.stream(existingCollectionIds)
160                                  .collect(Collectors.toSet());
161        }
162        
163        return collectionIds;
164    }
165    
166    /**
167     * Retrieves a query expression testing the collection
168     * @param collectionId the identifier of the collection to test
169     * @return the query expression
170     */
171    public Expression getCollectionExpression(String collectionId)
172    {
173        ExpressionContext context = ExpressionContext.newInstance()
174                                                     .withInternal(true);
175        
176        return new StringExpression(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME, Operator.EQ, collectionId, context);
177    }
178}