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