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