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