/******************************************************************************* * Imixs Workflow * Copyright (C) 2001, 2011 Imixs Software Solutions GmbH, * http://www.imixs.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You can receive a copy of the GNU General Public * License at http://www.gnu.org/licenses/gpl.html * * Project: * http://www.imixs.org * http://java.net/projects/imixs-workflow * * Contributors: * Imixs Software Solutions GmbH - initial API and implementation * Ralph Soika - Software Developer *******************************************************************************/ package org.imixs.marty.ejb; import java.io.Serializable; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map.Entry; import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.annotation.security.DeclareRoles; import javax.annotation.security.RolesAllowed; import javax.ejb.EJB; import javax.ejb.SessionContext; import javax.ejb.Singleton; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.PropertyService; import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.AccessDeniedException; import org.imixs.workflow.exceptions.InvalidAccessException; import org.imixs.workflow.exceptions.ModelException; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.ProcessingErrorException; import org.imixs.workflow.exceptions.QueryException; /** * The Marty ProfileService is a sigelton EJB providing user attributes like the * username and user email. The service is used to cache names application wide. * * * @author rsoika */ @DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.MANAGERACCESS" }) @RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.MANAGERACCESS" }) @Singleton public class ProfileService { public final static int START_PROFILE_PROCESS_ID = 200; public final static int CREATE_PROFILE_ACTIVITY_ID = 5; final int DEFAULT_CACHE_SIZE = 100; final int MAX_SEARCH_COUNT = 1; private Cache cache; private static Logger logger = Logger.getLogger(ProfileService.class.getName()); @EJB private DocumentService documentService; @EJB private PropertyService propertyService; @EJB protected WorkflowService workflowService; @Resource private SessionContext ctx; /** * PostContruct event - loads the imixs.properties. */ @PostConstruct void init() { // initialize cache... reset(); } /** * resets the profile cache.. */ public void reset() { // try to lookup cache size... int iCacheSize = DEFAULT_CACHE_SIZE; // early initialization did not work in Wildfly because of security // problem // see issue-#59 // try { // String sCacheSize = propertyService.getProperties().getProperty( // "profileservice.cachesize", DEFAULT_CACHE_SIZE + ""); // // // // iCacheSize = Integer.parseInt(sCacheSize); // } catch (NumberFormatException nfe) { // logger.warning("ProfileService unable to determine property: // profileservice.cachesize - check imixs.properties!"); // iCacheSize = DEFAULT_CACHE_SIZE; // } // initialize cache cache = new Cache(iCacheSize); } /** * This method returns the property 'profile.lowerCaseUserID'. The default * value is 'true' * * @return */ public boolean useLowerCaseUserID() { String value = propertyService.getProperties().getProperty("profile.lowerCaseUserID", "true"); if ("false".equals(value)) return false; else return true; } /** * This method returns a profile by its id. The method uses an internal * cache. The method returns null if no Profile for this name was found * * The returned workitem is a cloned version of the profile entity and can * not be processed or updated. Use lookupProfile to get the full entity of * a profile. * * @param userid * - the profile id * @return cloned workitem */ public ItemCollection findProfileById(String userid) { return findProfileById(userid, false); } /** * This method returns a profile by its id. The method uses an internal * cache. The method returns null if no Profile for this name was found. * * The returned workitem is a cloned version of the profile entity and can * not be processed or updated. Use lookupProfile to get the full entity of * a profile. * * If the boolean 'refresh' is true the method lookup the user in any case * with a JQPL statement and updates the cache. * * The userId is always lower case! * * @param userid * - the profile id * @param refresh * - boolean indicates if the internal cache should be used * @return cloned workitem */ public ItemCollection findProfileById(String userid, boolean refresh) { if (userid == null || userid.isEmpty()) { return null; } // lower case userId if (useLowerCaseUserID()) userid = userid.toLowerCase(); // try to get name out from cache ItemCollection userProfile = null; // use cache? if (!refresh) { logger.finest("lookup profile '" + userid + "' in cache..."); userProfile = (ItemCollection) cache.get(userid); } // not yet cached? if (userProfile == null && !cache.containsKey(userid)) { userProfile = lookupProfileById(userid); if (userProfile != null) { // put a clone workitem into the cahe userProfile = cloneWorkitem(userProfile); // cache profile cache.put(userid, userProfile); logger.fine("put profile '" + userid + "' into cache"); } else { logger.fine("profile '" + userid + "' not found, put 'null' into cache"); // put null entry into cache to avoid next lookup! cache.put(userid, null); } } else { logger.finest("get profile '" + userid + "' from cache"); } return userProfile; } /** * This method returns a profile by its id. In different to the * findProfileById method this method lookups the profile and returns the * full entity. The returned workItem can be processed. * * Use findProfileById to work with the internal cache if there is no need * to update the profile. * * The userId is always lower case! * * @param userid * - the profile id * @return profile workitem */ public ItemCollection lookupProfileById(String userid) { if (userid == null || userid.isEmpty()) { logger.warning("lookupProfileById - no id provided!"); return null; } // lower case userId if (useLowerCaseUserID()) { userid = userid.toLowerCase(); } // try to get name out from cache ItemCollection userProfile = null; logger.fine("lookupProfileById '" + userid + "'"); // lookup user profile.... String sQuery = "(type:\"profile\" AND txtname:\"" + userid + "\")"; logger.finest("searchprofile: " + sQuery); Collection<ItemCollection> col; try { col = documentService.find(sQuery, 1, 0); } catch (QueryException e) { throw new InvalidAccessException(InvalidAccessException.INVALID_ID, e.getMessage(), e); } if (col.size() > 0) { userProfile = col.iterator().next(); } else { logger.fine("lookup profile '" + userid + "' failed"); } return userProfile; } /** * This method removes a profile from the cache. * * @param userid */ public void discardCache(String userid) { cache.remove(userid); } /** * This method closes a profile entity and computes the attributes * txtUsername and txtInitials * * @param aWorkitem * @return */ public static ItemCollection cloneWorkitem(ItemCollection aWorkitem) { ItemCollection clone = new ItemCollection(); // clone the standard WorkItem properties clone.replaceItemValue("Type", aWorkitem.getItemValue("Type")); clone.replaceItemValue("$UniqueID", aWorkitem.getItemValue("$UniqueID")); clone.replaceItemValue("$ModelVersion", aWorkitem.getItemValue("$ModelVersion")); clone.replaceItemValue("$ProcessID", aWorkitem.getItemValue("$ProcessID")); clone.replaceItemValue("$Created", aWorkitem.getItemValue("$Created")); clone.replaceItemValue("$Modified", aWorkitem.getItemValue("$Modified")); clone.replaceItemValue("$isAuthor", aWorkitem.getItemValue("$isAuthor")); clone.replaceItemValue("txtWorkflowStatus", aWorkitem.getItemValue("txtWorkflowStatus")); clone.replaceItemValue("txtWorkflowSummary", aWorkitem.getItemValue("txtWorkflowSummary")); clone.replaceItemValue("txtWorkflowAbstract", aWorkitem.getItemValue("txtWorkflowAbstract")); clone.replaceItemValue("txtEmail", aWorkitem.getItemValue("txtEmail")); clone.replaceItemValue("namdeputy", aWorkitem.getItemValue("namdeputy")); clone.replaceItemValue("txtusericon", aWorkitem.getItemValue("txtusericon")); // get accountName String sAccountName = aWorkitem.getItemValueString("txtName"); clone.replaceItemValue("txtName", sAccountName); // test txtuserName String sUserName = aWorkitem.getItemValueString("txtUserName"); if (!sUserName.isEmpty()) { clone.replaceItemValue("txtUserName", sUserName); } else { // use account name clone.replaceItemValue("txtUserName", sAccountName); } // construct initials (2 digits) String sInitials = aWorkitem.getItemValueString("txtinitials"); if (sInitials.isEmpty()) { // default sInitials = "NO"; if (!sUserName.isEmpty() && sUserName.length() > 2) { int iPos = sUserName.indexOf(' '); if (iPos > -1) { sInitials = sUserName.substring(0, 1); sInitials = sInitials + sUserName.substring(iPos + 1, iPos + 2); } else { sInitials = sUserName.substring(0, 2); } } else { sInitials = sAccountName; } clone.replaceItemValue("txtinitials", sInitials); } else { clone.replaceItemValue("txtinitials", sInitials); } return clone; } /** * Creates a new profile document * * @param userid * @return * @throws ModelException * @throws PluginException * @throws ProcessingErrorException * @throws AccessDeniedException */ public ItemCollection createProfile(String userid, String locale) throws AccessDeniedException, ProcessingErrorException, PluginException, ModelException { logger.info("create new profile for userid '" + userid + "'.... "); // create new Profile for current user ItemCollection profile = new ItemCollection(); profile.replaceItemValue("type", "profile"); profile.replaceItemValue("$processID", START_PROFILE_PROCESS_ID); // get system model version from imixs.properties String modelVersion = this.propertyService.getProperties().getProperty("system.model.version", ""); profile.replaceItemValue("$modelversion", modelVersion); // the workflow group can not be guessed here... // profile.replaceItemValue("$workflowgroup", "Profil"); profile.replaceItemValue("txtName", userid); profile.replaceItemValue("txtLocale", locale); // set default group profile.replaceItemValue("txtgroups", "IMIXS-WORKFLOW-Author"); // process new profile... profile.replaceItemValue("$ActivityID", CREATE_PROFILE_ACTIVITY_ID); profile = workflowService.processWorkItem(profile); logger.fine("new profile created for userid '" + userid + "'"); return profile; } /** * Cache implementation to hold config entities * * @author rsoika * */ class Cache extends LinkedHashMap<String, ItemCollection> implements Serializable { private static final long serialVersionUID = 1L; private final int capacity; public Cache(int capacity) { super(capacity + 1, 1.1f, true); this.capacity = capacity; } protected boolean removeEldestEntry(Entry<String, ItemCollection> eldest) { return size() > capacity; } } }