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