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