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.userdirectory;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Iterator;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.activity.Initializable;
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.cocoon.components.ContextHelper;
037import org.apache.cocoon.environment.Request;
038import org.apache.commons.lang.StringUtils;
039
040import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
041import org.ametys.cms.data.ContentValue;
042import org.ametys.cms.languages.LanguagesManager;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.repository.ContentQueryHelper;
045import org.ametys.cms.repository.ContentTypeExpression;
046import org.ametys.cms.repository.LanguageExpression;
047import org.ametys.core.cache.AbstractCacheManager;
048import org.ametys.core.cache.Cache;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.plugins.contentio.synchronize.rights.SynchronizedRootContentHelper;
051import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
055import org.ametys.plugins.repository.RepositoryConstants;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057import org.ametys.plugins.repository.collection.AmetysObjectCollection;
058import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
059import org.ametys.plugins.repository.query.expression.AndExpression;
060import org.ametys.plugins.repository.query.expression.Expression;
061import org.ametys.plugins.repository.query.expression.Expression.Operator;
062import org.ametys.plugins.repository.query.expression.UserExpression;
063import org.ametys.runtime.i18n.I18nizableText;
064import org.ametys.runtime.plugin.component.AbstractLogEnabled;
065
066/**
067 * Helper for user directory.
068 */
069public class UserDirectoryHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable
070{
071    /** The avalon role. */
072    public static final String ROLE = UserDirectoryHelper.class.getName();
073    
074    /** The root node name of the plugin */
075    public static final String USER_DIRECTORY_ROOT_NODE = "userdirectory";
076    
077    /** The orgUnit parent attribute */
078    public static final String ORGUNITS_ATTRIBUTE = "orgunits";
079    
080    /** The orgUnit content type */
081    public static final String ORGUNIT_CONTENT_TYPE = "org.ametys.plugins.userdirectory.Content.udorgunit";
082    
083    /** The parent content type id */
084    public static final String ABSTRACT_USER_CONTENT_TYPE = "org.ametys.plugins.userdirectory.Content.user";
085    
086    /** The ametys object resolver */
087    protected AmetysObjectResolver _resolver;
088    
089    /** The content type extension point */
090    protected ContentTypeExtensionPoint _cTypeEP;
091    
092    /** The cache manager */
093    protected AbstractCacheManager _cacheManager;
094    
095    /** The languages manager */
096    protected LanguagesManager _languagesManager;
097    
098    /** The context */
099    protected Context _context;
100    
101    static class UserElementKey extends AbstractCacheKey
102    {
103        UserElementKey(String lang, UserIdentity userIdentity)
104        {
105            super(lang, userIdentity);
106        }
107        
108        static UserElementKey of(String lang, UserIdentity userIdentity)
109        {
110            return new UserElementKey(lang, userIdentity);
111        }
112        
113        static UserElementKey of(String lang)
114        {
115            return new UserElementKey(lang, null);
116        }
117    }
118    
119    @Override
120    public void service(ServiceManager manager) throws ServiceException
121    {
122        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
123        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
124        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
125        _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
126    }
127    
128    @Override
129    public void initialize() throws Exception
130    {
131        _cacheManager.createMemoryCache(ROLE,
132                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_USER_CACHE_LABEL"),
133                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_USER_CACHE_DESCRIPTION"),
134                true,
135                null);
136    }
137    
138    public void contextualize(Context context) throws ContextException
139    {
140        _context = context;
141    }
142    
143    /**
144     * Gets the root of user directory contents
145     * @return the root of user directory contents
146     */
147    public AmetysObjectCollection getUserDirectoryRootContent()
148    {
149        return getUserDirectoryRootContent(false);
150    }
151    
152    /**
153     * Gets the root of user directory contents
154     * @param create <code>true</code> to create automatically the root when missing.
155     * @return the root of user directory contents
156     */
157    public AmetysObjectCollection getUserDirectoryRootContent(boolean create)
158    {
159        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/");
160        
161        boolean needSave = false;
162        if (!pluginsNode.hasChild(USER_DIRECTORY_ROOT_NODE))
163        {
164            if (create)
165            {
166                pluginsNode.createChild(USER_DIRECTORY_ROOT_NODE, "ametys:unstructured");
167                needSave = true;
168            }
169            else
170            {
171                throw new UnknownAmetysObjectException("Node '/ametys:plugins/userdirectory' is missing");
172            }
173        }
174        
175        ModifiableTraversableAmetysObject userdirectoryNode = pluginsNode.getChild(USER_DIRECTORY_ROOT_NODE);
176        if (!userdirectoryNode.hasChild(SynchronizedRootContentHelper.IMPORTED_CONTENTS_ROOT_NODE))
177        {
178            if (create)
179            {
180                userdirectoryNode.createChild(SynchronizedRootContentHelper.IMPORTED_CONTENTS_ROOT_NODE, "ametys:collection");
181                needSave = true;
182            }
183            else
184            {
185                throw new UnknownAmetysObjectException("Node '/ametys:plugins/userdirectory/ametys:contents' is missing");
186            }
187        }
188        
189        if (needSave)
190        {
191            pluginsNode.saveChanges();
192        }
193        
194        return userdirectoryNode.getChild(SynchronizedRootContentHelper.IMPORTED_CONTENTS_ROOT_NODE);
195    }
196    
197    /**
198     * Get the list of orgunits content
199     * @param user the user content
200     * @return the list of orgunits
201     */
202    public List<Content> getOrgUnits(Content user)
203    {
204        ContentValue[] contents = user.getValue(ORGUNITS_ATTRIBUTE);
205        if (contents == null)
206        {
207            return List.of();
208        }
209        
210        return Arrays.stream(contents)
211                     .map(v -> v.getContentIfExists())
212                     .flatMap(Optional::stream)
213                     .collect(Collectors.toList());
214    }
215    
216    /**
217     * Get all user content for the identity in all languages
218     * @param user the identity
219     * @return the user contents
220     */
221    public List<Content> getUserContents(UserIdentity user)
222    {
223        return getUserContents(user, null);
224    }
225    
226    /**
227     * Get user contents from user identity
228     * @param user the user identity
229     * @param givenLanguage the given language. if null, all contents from all available languages are returned
230     * @return the user contents
231     */
232    public List<Content> getUserContents(UserIdentity user, String givenLanguage)
233    {
234        List<Content> userContents = new ArrayList<>();
235        Iterator<String> languagesIterator = _getLanguagesToTest(givenLanguage).iterator();
236        while (languagesIterator.hasNext()
237            && (userContents.isEmpty() || givenLanguage == null))
238        {
239            String lang = languagesIterator.next();
240            UserElementKey key = UserElementKey.of(lang, user);
241            List<String> contentIds = _getCache().get(key, k -> _requestUserContentIds(user, lang));
242            
243            userContents = contentIds.stream()
244                    .map(id -> this._resolveContent(id, lang))
245                    .filter(Objects::nonNull)
246                    .toList();
247        }
248        
249        return userContents;
250    }
251    
252    private Content _resolveContent(String contentId, String lang)
253    {
254        try
255        {
256            return contentId != null ? _resolver.resolveById(contentId) : null;
257        }
258        catch (UnknownAmetysObjectException e)
259        {
260            // The content's id is always retrieved in default workspace: content may not exist in current workspace
261            getLogger().warn("User content with id '{}' was not found in current workspace for language '{}'", contentId, lang);
262        }
263        
264        return null;
265    }
266    
267    /**
268     * Get user content from user identity
269     * @param user the user identity
270     * @param givenLanguage the given language
271     * @return the user content (null if it not exist)
272     */
273    public Content getUserContent(UserIdentity user, String givenLanguage)
274    {
275        List<Content> userContents = getUserContents(user, givenLanguage);
276        return userContents.isEmpty() ? null : userContents.get(0);
277    }
278    
279    private Set<String> _getLanguagesToTest(String givenLang)
280    {
281        Set<String> languages = new LinkedHashSet<>();
282        
283        // First, test the given language if not blank
284        if (StringUtils.isNotBlank(givenLang))
285        {
286            languages.add(givenLang);
287        }
288        
289        // Then test english language
290        languages.add("en");
291        
292        // Finaly test other languages
293        languages.addAll(_languagesManager.getAvailableLanguages().keySet());
294        
295        return languages;
296    }
297
298    /**
299     * Request the list of user content id
300     * @param user the user identity
301     * @param lang the lang
302     * @return the list of user content id (empty if it not exist)
303     */
304    protected List<String> _requestUserContentIds(UserIdentity user, String lang)
305    {
306        Request request = ContextHelper.getRequest(_context);
307        
308        // Retrieve current workspace
309        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
310        
311        try
312        {
313            // Use default workspace
314            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
315         
316            // Content type condition
317            List<String> cTypeIds = new ArrayList<>();
318            cTypeIds.add(ABSTRACT_USER_CONTENT_TYPE);
319            cTypeIds.addAll(_cTypeEP.getSubTypes(ABSTRACT_USER_CONTENT_TYPE));
320            Expression cTyPeExpr = new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()]));
321            
322            // Lang condition
323            Expression langExpr = new LanguageExpression(Operator.EQ, lang);
324            // Login condition
325            Expression userExpr = new UserExpression("user", Operator.EQ, user);
326            
327            Expression finalExpr = new AndExpression(cTyPeExpr, langExpr, userExpr);
328            String query = ContentQueryHelper.getContentXPathQuery(finalExpr);
329            
330            AmetysObjectIterable<Content> contents = _resolver.query(query);
331            return contents.stream()
332                    .map(Content::getId)
333                    .toList();
334        }
335        finally
336        {
337            // Restore context
338            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
339        }
340    }
341    
342    private Cache<UserElementKey, List<String>> _getCache()
343    {
344        return _cacheManager.get(ROLE);
345    }
346    
347    /**
348     * Invalidate the user content cache for one language. If lang is <code>null</code>, invalidate all
349     * @param lang the lang. Can be <code>null</code>
350     */
351    public void invalidateUserContentCache(String lang)
352    {
353        if (StringUtils.isNotBlank(lang))
354        {
355            _getCache().invalidate(UserElementKey.of(lang));
356        }
357        else
358        {
359            _getCache().invalidateAll();
360        }
361    }
362}