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        collectionIds.add(collectionId);
147
148        content.getInternalDataHolder().setValue(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME, collectionIds.toArray(new String[collectionIds.size()]));
149    }
150    
151    /**
152     * Update the given content's synchronization properties
153     * @param content the synchronized content
154     */
155    public void updateLastSynchronizationProperties(Content content)
156    {
157        content.getInternalDataHolder().setValue(SynchronizableContentsCollection.LAST_SYNCHRONIZATION_DATA_NAME, ZonedDateTime.now(), ModelItemTypeConstants.DATETIME_TYPE_ID);
158        content.getInternalDataHolder().setValue(SynchronizableContentsCollection.LAST_SYNCHRONIZATION_USER_DATA_NAME, _currentUserProvider.getUser(), org.ametys.cms.data.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID);
159    }
160    
161    /**
162     * Retrieves the synchronizable collection identifiers
163     * @param content the content
164     * @return the synchronizable collection identifiers
165     * @throws AmetysRepositoryException if an error occurs while reading SCC info on the given content
166     */
167    public Set<String> getSynchronizableCollectionIds(Content content) throws AmetysRepositoryException
168    {
169        ModelLessDataHolder internalDataHolder = content.getInternalDataHolder();
170        
171        Set<String> collectionIds = new HashSet<>();
172        if (internalDataHolder.hasValue(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME))
173        {
174            String[] existingCollectionIds = internalDataHolder.getValue(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME);
175            collectionIds = Arrays.stream(existingCollectionIds)
176                                  .collect(Collectors.toSet());
177        }
178        
179        return collectionIds;
180    }
181    
182    /**
183     * Retrieves a query expression testing the collection
184     * @param collectionId the identifier of the collection to test
185     * @return the query expression
186     */
187    public Expression getCollectionExpression(String collectionId)
188    {
189        ExpressionContext context = ExpressionContext.newInstance()
190                                                     .withInternal(true);
191        
192        return new StringExpression(SynchronizableContentsCollection.COLLECTION_ID_DATA_NAME, Operator.EQ, collectionId, context);
193    }
194}