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 attribute */ 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}