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