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 user contents from user identity 218 * @param user the user identity 219 * @param givenLanguage the given language 220 * @return the user contents 221 */ 222 public List<Content> getUserContents(UserIdentity user, String givenLanguage) 223 { 224 List<Content> userContents = new ArrayList<>(); 225 Iterator<String> languagesIterator = _getLanguagesToTest(givenLanguage).iterator(); 226 while (userContents.isEmpty() && languagesIterator.hasNext()) 227 { 228 String lang = languagesIterator.next(); 229 UserElementKey key = UserElementKey.of(lang, user); 230 List<String> contentIds = _getCache().get(key, k -> _requestUserContentIds(user, lang)); 231 232 userContents = contentIds.stream() 233 .map(id -> this._resolveContent(id, lang)) 234 .filter(Objects::nonNull) 235 .toList(); 236 } 237 238 return userContents; 239 } 240 241 private Content _resolveContent(String contentId, String lang) 242 { 243 try 244 { 245 return contentId != null ? _resolver.resolveById(contentId) : null; 246 } 247 catch (UnknownAmetysObjectException e) 248 { 249 // The content's id is always retrieved in default workspace: content may not exist in current workspace 250 getLogger().warn("User content with id '{}' was not found in current workspace for language '{}'", contentId, lang); 251 } 252 253 return null; 254 } 255 256 /** 257 * Get user content from user identity 258 * @param user the user identity 259 * @param givenLanguage the given language 260 * @return the user content (null if it not exist) 261 */ 262 public Content getUserContent(UserIdentity user, String givenLanguage) 263 { 264 List<Content> userContents = getUserContents(user, givenLanguage); 265 return userContents.isEmpty() ? null : userContents.get(0); 266 } 267 268 private Set<String> _getLanguagesToTest(String givenLang) 269 { 270 Set<String> languages = new LinkedHashSet<>(); 271 272 // First, test the given language if not blank 273 if (StringUtils.isNotBlank(givenLang)) 274 { 275 languages.add(givenLang); 276 } 277 278 // Then test english language 279 languages.add("en"); 280 281 // Finaly test other languages 282 languages.addAll(_languagesManager.getAvailableLanguages().keySet()); 283 284 return languages; 285 } 286 287 /** 288 * Request the list of user content id 289 * @param user the user identity 290 * @param lang the lang 291 * @return the list of user content id (empty if it not exist) 292 */ 293 protected List<String> _requestUserContentIds(UserIdentity user, String lang) 294 { 295 Request request = ContextHelper.getRequest(_context); 296 297 // Retrieve current workspace 298 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 299 300 try 301 { 302 // Use default workspace 303 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 304 305 // Content type condition 306 List<String> cTypeIds = new ArrayList<>(); 307 cTypeIds.add(ABSTRACT_USER_CONTENT_TYPE); 308 cTypeIds.addAll(_cTypeEP.getSubTypes(ABSTRACT_USER_CONTENT_TYPE)); 309 Expression cTyPeExpr = new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()])); 310 311 // Lang condition 312 Expression langExpr = new LanguageExpression(Operator.EQ, lang); 313 // Login condition 314 Expression userExpr = new UserExpression("user", Operator.EQ, user); 315 316 Expression finalExpr = new AndExpression(cTyPeExpr, langExpr, userExpr); 317 String query = ContentQueryHelper.getContentXPathQuery(finalExpr); 318 319 AmetysObjectIterable<Content> contents = _resolver.query(query); 320 return contents.stream() 321 .map(Content::getId) 322 .toList(); 323 } 324 finally 325 { 326 // Restore context 327 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 328 } 329 } 330 331 private Cache<UserElementKey, List<String>> _getCache() 332 { 333 return _cacheManager.get(ROLE); 334 } 335 336 /** 337 * Invalidate the user content cache for one language. If lang is <code>null</code>, invalidate all 338 * @param lang the lang. Can be <code>null</code> 339 */ 340 public void invalidateUserContentCache(String lang) 341 { 342 if (StringUtils.isNotBlank(lang)) 343 { 344 _getCache().invalidate(UserElementKey.of(lang)); 345 } 346 else 347 { 348 _getCache().invalidateAll(); 349 } 350 } 351}