/******************************************************************************* * 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.plugins; import java.util.Collection; import java.util.List; import java.util.Vector; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import org.imixs.marty.ejb.ProfileService; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.WorkflowContext; import org.imixs.workflow.engine.plugins.AbstractPlugin; import org.imixs.workflow.exceptions.InvalidAccessException; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; /** * This plug-in supports additional business logic for profile entities. This * Plugins is used by the System Workflow when a userProfile is processed * (typically when a User logged in). * * In additon the Plugin provides a mechanism to translate elements of an * activityEntity to replace placeholders for a user id with the corresponding * user name. There for the plugin uses the profileService EJB * * Example: * * <code> * Workitem updated by: <username>namcurrenteditor</username>. * </code> * * This will replace the namcurrenteditor with the corrsponding profile full * username * * @author rsoika * */ public class ProfilePlugin extends AbstractPlugin { private ProfileService profileService = null; private static Logger logger = Logger.getLogger(ProfilePlugin.class .getName()); // error codes public static String USERNAME_ALREADY_TAKEN = "USERNAME_ALREADY_TAKEN"; public static String INVALID_USERNAME = "INVALID_USERNAME"; public static String EMAIL_ALREADY_TAKEN = "EMAIL_ALREADY_TAKEN"; public static String INVALID_EMAIL = "INVALID_EMAIL"; public static String NO_PROFILE_SERVICE_FOUND = "NO_PROFILE_SERVICE_FOUND"; public static String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; public static String USERID_PATTERN = "^[A-Za-z0-9.@\\-\\w]+"; @Override public void init(WorkflowContext actx) throws PluginException { super.init(actx); // lookup profile service EJB String jndiName = "ejb/ProfileService"; InitialContext ictx; try { ictx = new InitialContext(); Context ctx = (Context) ictx.lookup("java:comp/env"); profileService = (ProfileService) ctx.lookup(jndiName); } catch (NamingException e) { throw new PluginException(ProfilePlugin.class.getSimpleName(), NO_PROFILE_SERVICE_FOUND, "[ProfilePlugin] unable to lookup profileService: ", e); } } /** * The Plug-in verifies if the workitem is from the type 'profile'. The * plug-in tests if the usernam or email is unique **/ @Override public ItemCollection run(ItemCollection workItem, ItemCollection documentActivity) throws PluginException { // validate profile.. if ("profile".equals(workItem.getItemValueString("type"))) { validateUserProfile(workItem); // discared cache for this name profileService.discardCache(workItem.getItemValueString("txtName")); } // translate dynamic activity values - (this is independent form the type of the workitem) updateActivityEntity(workItem, documentActivity); return workItem; } /** * replace the text phrases in the activity * * @param workItem * @param documentActivity */ void updateActivityEntity(ItemCollection workItem, ItemCollection documentActivity) { String sText; String[] fields = { "rtfresultlog", "txtworkflowabstract", "txtworkflowsummary", "txtMailSubject", "rtfMailBody" }; for (String aField : fields) { sText = documentActivity.getItemValueString(aField); sText = replaceUsernames(sText, workItem); documentActivity.replaceItemValue(aField, sText); } } /** * The method validates the userProfile entity. The txtName property will be * initialized if a new profile is created The txtName property will always * be lower case! * * @param profile * @throws PluginException */ void validateUserProfile(ItemCollection profile) throws PluginException { Pattern pattern; Matcher matcher; String sUsername = profile.getItemValueString("txtName"); String sEmail = profile.getItemValueString("txtEmail"); if (this.getWorkflowService().getUserName() == null || this.getWorkflowService().getUserName().isEmpty()) { throw new PluginException(ProfilePlugin.class.getSimpleName(), INVALID_USERNAME, "Invalid username - username is empty"); } // update the txtname if not already set if ("".equals(sUsername)) { // trim and lower case username! sUsername = this.getWorkflowService().getUserName().toLowerCase().trim(); logger.fine("initialize profile with username: " + sUsername); profile.replaceItemValue("txtName", sUsername); } // verify email pattern if (!sEmail.isEmpty()) { pattern = Pattern.compile(EMAIL_PATTERN); matcher = pattern.matcher(sEmail); if (!matcher.matches()) { throw new PluginException(ProfilePlugin.class.getSimpleName(), INVALID_EMAIL, "Invalid Email Address", new Object[] { profile.getItemValueString("txtEmail") }); } } // verify userid pattern pattern = Pattern.compile(USERID_PATTERN); matcher = pattern.matcher(sUsername); if (!matcher.matches()) { throw new PluginException(ProfilePlugin.class.getSimpleName(), INVALID_USERNAME, "Invalid Username", new Object[] { profile.getItemValueString("txtName") }); } if (!isValidUserName(profile)) throw new PluginException( ProfilePlugin.class.getSimpleName(), USERNAME_ALREADY_TAKEN, "Username is already taken - verifiy txtname and txtusername", new Object[] { profile.getItemValueString("txtName") }); if (!isValidEmail(profile)) throw new PluginException(ProfilePlugin.class.getSimpleName(), EMAIL_ALREADY_TAKEN, "Email is already taken - verifiy txtemail", new Object[] { profile.getItemValueString("txtEmail") }); } /** * verifies if the txtName and txtUsername is available. Attribute * txtUsername is optional and will be only verified if provided. * * returns true if name isn't still taken by another object. * * @param aprofile * @return */ boolean isValidUserName(ItemCollection profile) { String sName = profile.getItemValueString("txtName"); String sUserName = profile.getItemValueString("txtUserName"); String sID = profile.getItemValueString("$uniqueid"); // Trim names.... if (!sName.equals(sName.trim())) { sName = sName.trim(); profile.replaceItemValue("txtName", sName); } // lower case userid? if ((profileService.useLowerCaseUserID()) && (!sName.equals(sName.toLowerCase()))) { sName = sName.toLowerCase(); profile.replaceItemValue("txtName", sName); } if (!sUserName.equals(sUserName.trim())) { sUserName = sUserName.trim(); profile.replaceItemValue("txtUserName", sUserName); } String sQuery; // username provided? if (sUserName != null && !"".equals(sUserName)) { // sQuery = "SELECT DISTINCT profile FROM Entity as profile " // + " JOIN profile.textItems AS n" // + " JOIN profile.textItems AS u" // + " WHERE profile.type = 'profile' " // + " AND ((n.itemName = 'txtname' " + " AND n.itemValue = '" // + sName + "') OR (u.itemName = 'txtusername' " // + " AND u.itemValue = '" + sUserName + "'))" // + " AND profile.id<>'" + sID + "' "; sQuery="(type:\"profile\" AND (txtname:\""+sName + "\" OR txtusername:\""+sUserName + "\")) AND (NOT $uniqueid:\"" + sID + "\")"; } else { // query only txtName // sQuery = "SELECT DISTINCT profile FROM Entity as profile " // + " JOIN profile.textItems AS n" + " WHERE profile.id<>'" // + sID + "' AND profile.type = 'profile' " // + " AND n.itemName = 'txtname' " + " AND n.itemValue = '" // + sName + "'"; sQuery="(type:\"profile\" AND txtname:\""+sName + "\") AND (NOT $uniqueid:\"" + sID + "\")"; } Collection<ItemCollection> col; try { col = this.getWorkflowService().getDocumentService().find(sQuery,1, 0); } catch (QueryException e) { throw new InvalidAccessException(InvalidAccessException.INVALID_ID,e.getMessage(),e); } return (col.size() == 0); } /** * verifies if the txtemail is available. returns true if address isn't * still taken by another object. * * @param aprofile * @return */ boolean isValidEmail(ItemCollection profile) { String sEmail = profile.getItemValueString("txtEmail"); String sID = profile.getItemValueString("$uniqueid"); // Trim email.... if (!sEmail.equals(sEmail.trim())) { sEmail = sEmail.trim(); profile.replaceItemValue("txtEmail", sEmail); } String sQuery; // username provided? if (!"".equals(sEmail)) { // sQuery = "SELECT DISTINCT profile FROM Entity as profile " // + " JOIN profile.textItems AS n" // + " WHERE profile.type = 'profile' " // + " AND (n.itemName = 'txtemail' " + " AND n.itemValue = '" // + sEmail + "') " + " AND profile.id<>'" + sID + "' "; sQuery="(type:\"profile\" AND txtemail:\""+sEmail + "\") AND (NOT $uniqueid:\"" + sID + "\")"; } else { return true; } Collection<ItemCollection> col; try { col = this.getWorkflowService().getDocumentService().find(sQuery,1, 0); } catch (QueryException e) { throw new InvalidAccessException(InvalidAccessException.INVALID_ID,e.getMessage(),e); } return (col.size() == 0); } /** * Check for special characters * * @param profile * @return true if special character found */ boolean isValidUserNameInput(ItemCollection profile) { String sName = profile.getItemValueString("txtName"); // if (Pattern.matches( "'.*'", "'Hallo Welt" )) if (sName.contains("ö")) return true; if (sName.contains("ü")) return true; if (sName.contains("ä")) return true; if (sName.contains("Ü")) return true; if (sName.contains("Ö")) return true; if (sName.contains("Ä")) return true; if (sName.contains("ß")) return true; if (sName.contains("ö")) return true; if (sName.contains("ö")) return true; if (sName.contains("ö")) return true; return false; } /** * this method parses a string for xml tag <username>. Those tags will be * replaced with the corresponding userProfile property 'txtUserName' <code> * * hello <username>namCreator</username> * * * </code> * * * If the itemValue is a multiValue object the single values can be * spearated by a separator * * <code> * * Team List: <username separator="<br />">txtTeam</username> * * </code> * * * */ @SuppressWarnings({ "rawtypes", "unchecked" }) public String replaceUsernames(String aString, ItemCollection documentContext) { int iTagStartPos; int iTagEndPos; int iContentStartPos; int iContentEndPos; int iSeparatorStartPos; int iSeparatorEndPos; String sSeparator = " "; String sItemValue; if (aString == null) return ""; // test if a <value> tag exists... while ((iTagStartPos = aString.toLowerCase().indexOf("<username")) != -1) { iTagEndPos = aString.toLowerCase().indexOf("</username>", iTagStartPos); // if no end tag found return string unchanged... if (iTagEndPos == -1) return aString; // reset pos vars iContentStartPos = 0; iContentEndPos = 0; iSeparatorStartPos = 0; iSeparatorEndPos = 0; sSeparator = " "; sItemValue = ""; // so we now search the beginning of the tag content iContentEndPos = iTagEndPos; // start pos is the last > before the iContentEndPos String sTestString = aString.substring(0, iContentEndPos); iContentStartPos = sTestString.lastIndexOf('>') + 1; // if no end tag found return string unchanged... if (iContentStartPos >= iContentEndPos) return aString; iTagEndPos = iTagEndPos + "</username>".length(); // now we have the start and end position of a tag and also the // start and end pos of the value // next we check if the start tag contains a 'separator' // attribute iSeparatorStartPos = aString.toLowerCase().indexOf("separator=", iTagStartPos); // extract format string if available if (iSeparatorStartPos > -1 && iSeparatorStartPos < iContentStartPos) { iSeparatorStartPos = aString.indexOf("\"", iSeparatorStartPos) + 1; iSeparatorEndPos = aString .indexOf("\"", iSeparatorStartPos + 1); sSeparator = aString.substring(iSeparatorStartPos, iSeparatorEndPos); } // extract Item Value sItemValue = aString.substring(iContentStartPos, iContentEndPos); List<String> tempList = documentContext.getItemValue(sItemValue); // clone List List<String> vUserIDs = new Vector(tempList); // get usernames .... for (int i = 0; i < vUserIDs.size(); i++) { ItemCollection profile = profileService .findProfileById(vUserIDs.get(i)); if (profile != null) { vUserIDs.set(i, profile.getItemValueString("txtUserName")); } } // format field value String sResult = formatItemValues(vUserIDs, sSeparator, ""); // now replace the tag with the result string aString = aString.substring(0, iTagStartPos) + sResult + aString.substring(iTagEndPos); } return aString; } }