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}