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}