org.opentaps.common.util.UtilCommon.java Source code

Java tutorial

Introduction

Here is the source code for org.opentaps.common.util.UtilCommon.java

Source

/*
 * Copyright (c) Open Source Strategies, Inc.
 *
 * Opentaps is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Opentaps 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Opentaps.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.opentaps.common.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Timestamp;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import javolution.util.FastList;
import javolution.util.FastMap;
import javolution.util.FastSet;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFDataFormat;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.ofbiz.base.component.ComponentConfig;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.GeneralException;
import org.ofbiz.base.util.StringUtil;
import org.ofbiz.base.util.UtilDateTime;
import org.ofbiz.base.util.UtilHttp;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.base.util.string.FlexibleStringExpander;
import org.ofbiz.content.data.DataResourceWorker;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityExpr;
import org.ofbiz.entity.condition.EntityOperator;
import org.ofbiz.entity.model.ModelEntity;
import org.ofbiz.entity.util.ByteWrapper;
import org.ofbiz.entity.util.EntityFindOptions;
import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.ModelService;
import org.opentaps.foundation.entity.EntityInterface;

/**
 * UtilCommon - A place for common opentaps helper methods.
 *
 * @author     <a href="mailto:leon@opensourcestrategies.com">Leon Torres</a>
 * @version    $Rev: 488 $
 */
public abstract class UtilCommon {

    private static final String MODULE = UtilCommon.class.getName();
    public static String SESSION_ID_PARTTER = ";jsessionid=[^\\.]*\\.jvm1";
    public static String PARAMETER_SEPARATE_CHAR_PARTTER = "[\\?\\&]";

    // Utility class should not be instantiated.
    private UtilCommon() {
    }

    /** Frequently used EntityFindOption for distinct read-only select. */
    public static final EntityFindOptions DISTINCT_READ_OPTIONS = new EntityFindOptions(true,
            EntityFindOptions.TYPE_SCROLL_INSENSITIVE, EntityFindOptions.CONCUR_READ_ONLY, true);

    /** Read only without distinct. */
    public static final EntityFindOptions READ_ONLY_OPTIONS = new EntityFindOptions(true,
            EntityFindOptions.TYPE_SCROLL_INSENSITIVE, EntityFindOptions.CONCUR_READ_ONLY, false);

    // Following int constants are mainly used to make the values non ambiguous (some methods may take MS, other Seconds ...) in the code and increase readability.

    /** Number of milliseconds in one second. For use in before / after methods for example. */
    public static final int MSEC_IN_1_SEC = 1000;
    /** Number of milliseconds in one minute. For use in before / after methods for example. */
    public static final int MSEC_IN_1_MIN = 60000;
    /** Number of seconds in one hour. For use in overriding transactions timeout for example. */
    public static final int SEC_IN_1_HOUR = 3600;
    /** Number of seconds in one hour. For use in overriding transactions timeout for example. */
    public static final int SEC_IN_2_HOURS = 7200;

    /**
     * Checks if the two lists are equivalent.
     * Returns true if:
     * <ul>
     *  <li>l1 and l2 are both null</li>
     *  <li>l1 and l2 are the same size, and every element in l1 is contained in l2, and every element in l2 is contained in l2</li>
     * </ul>
     * @param l1 first <code>List</code>
     * @param l2 second <code>List</code>
     * @return are the two list equivalent
     */
    @SuppressWarnings("unchecked")
    public static boolean isEquivalent(List l1, List l2) {
        // initial checks
        if ((l1 == null) && (l2 == null)) {
            return true; // if they are both null, they are equivalent
        }
        if ((l1 == null) || (l2 == null)) {
            return false; // otherwise, if one is null and the other is not, they are not equivalent
        }
        if (l1.size() != l2.size()) {
            return false; // sizes unequal - not equivalent
        }
        if ((l1.size() == 0) && (l2.size() == 0)) {
            return true; // both no elements - equivalent
        }

        // now loop through both lists and make sure they all contain the same elements
        for (Object element : l1) {
            if ((!l2.contains(element))) {
                return false;
            }
        }

        // since both list have the same number of elements and l2 contains all the elements of l1, then l2 contains all the elements of l1 too
        return true;
    }

    /**
     * Returns the "system" userLogin generic value.
     * @param delegator a <code>Delegator</code>
     * @return the <code>GenericValue</code> representing the "system" UserLogin entity.
     * @throws GenericEntityException if an error occurs
     */
    public static GenericValue getSystemUserLogin(Delegator delegator) throws GenericEntityException {
        return delegator.findByPrimaryKeyCache("UserLogin", UtilMisc.toMap("userLoginId", "system"));
    }

    /**
     * Returns the earliest of the two given <code>Timestamp</code>.
     * @param ts1 a <code>Timestamp</code>
     * @param ts2 another <code>Timestamp</code>
     * @return ts1 if it is before ts2, ts2 otherwise
     */
    public static Timestamp earlierOf(Timestamp ts1, Timestamp ts2) {
        if (ts1.before(ts2)) {
            return ts1;
        } else {
            return ts2;
        }
    }

    /**
     * Returns the latest of the two given <code>Timestamp</code>.
     * @param ts1 a <code>Timestamp</code>
     * @param ts2 another <code>Timestamp</code>
     * @return ts1 if it is after ts2, ts2 otherwise
     */
    public static Timestamp laterOf(Timestamp ts1, Timestamp ts2) {
        if (ts1.after(ts2)) {
            return ts1;
        } else {
            return ts2;
        }
    }

    /**
     * Gets the duration between two timestamps in localized format.
     * @param start a <code>Timestamp</code> value
     * @param end a <code>Timestamp</code> value
     * @param timeZone a <code>TimeZone</code> value
     * @param locale a <code>Locale</code> value
     * @return a <code>String</code> value
     */
    public static String getDuration(Timestamp start, Timestamp end, TimeZone timeZone, Locale locale) {
        Calendar cal = Calendar.getInstance(timeZone, locale);

        // set the time to the beginning of the day
        cal.setTime(UtilDateTime.getDayStart(start, timeZone, locale));

        // the duration in milliseconds
        long duration = end.getTime() - start.getTime();

        // compute number of full hours
        int hours = (int) (duration / 1000 / 60 / 60);

        // compute number of minutes
        int minutes = (int) ((duration / 1000 / 60) % 60);

        //TODO: oandreyev. Currently this way to make formated time is suitable but
        // algorithm should be refactored and based on applicable time format.
        return hours + ":" + (minutes < 10 ? "0" : "") + minutes;
    }

    /**
     * This method takes the date/time/duration form input and transforms it into an end timestamp.
     * It uses Java Date formatting capabilities to transform the duration input into an interval.
     *
     * @param start Full date, hour, minute and second of the starting time
     * @param duration The user input for hour such as "1:00"
     * @param timeZone a <code>TimeZone</code> value
     * @param locale a <code>Locale</code> value
     * @return the end <code>Timestamp</code>
     * @throws IllegalArgumentException If the duration input is unparseable or negative
     */
    public static Timestamp getEndTimestamp(Timestamp start, String duration, Locale locale, TimeZone timeZone)
            throws IllegalArgumentException {

        // return the start timestamp if no duration specified (i.e., duration = 0)
        if (duration == null || duration.length() == 0) {
            return start;
        }

        Calendar cal = Calendar.getInstance(timeZone, locale);

        // Turn the duraiton into a date and time with the hour being the duration (note this is input from user, which we require to be in HH:mm form)
        SimpleDateFormat df = new SimpleDateFormat("HH:mm");
        try {
            cal.setTime(df.parse(duration));
        } catch (ParseException e) {
            throw new IllegalArgumentException(
                    String.format("Duration input must be in %1$s format.", UtilDateTime.getTimeFormat(locale)));
        }

        // extract the days, hours and minutes
        int days = cal.get(Calendar.DAY_OF_YEAR) - 1;
        int hours = cal.get(Calendar.HOUR_OF_DAY);
        int minutes = cal.get(Calendar.MINUTE);

        // set to the start time and add the hours and minutes
        cal.setTime(start);
        cal.set(Calendar.DAY_OF_YEAR, days + cal.get(Calendar.DAY_OF_YEAR));
        cal.set(Calendar.HOUR_OF_DAY, hours + cal.get(Calendar.HOUR_OF_DAY));
        cal.set(Calendar.MINUTE, minutes + cal.get(Calendar.MINUTE));

        // create the end timestamp
        Timestamp end = new Timestamp(cal.getTimeInMillis());

        // make sure it's after the start timestamp
        if (end.before(start)) {
            throw new IllegalArgumentException("Cannot set a negative duration.");
        }

        // return our result as a Timestamp
        return end;
    }

    /**
     * Gets the <code>Timestamp</code> which time is the given milliseconds before the given <code>Timestamp</code>.
     * @param ts the reference <code>Timestamp</code>
     * @param milliseconds number of milliseconds before ts for which to return
     * @return a <code>Timestamp</code>, <code>null</code> if ts is <code>null</code>
     */
    public static Timestamp beforeMillisecs(Timestamp ts, long milliseconds) {
        if (ts != null) {
            return new Timestamp(ts.getTime() - milliseconds);
        } else {
            return null;
        }
    }

    /**
     * Gets the <code>Timestamp</code> which time is the given milliseconds before the given <code>Timestamp</code>.
     * @param ts the reference <code>Timestamp</code>
     * @param milliseconds number of milliseconds before ts for which to return
     * @return a <code>Timestamp</code>, <code>null</code> if ts is <code>null</code>
     */
    public static Timestamp beforeMillisecs(Timestamp ts, BigDecimal milliseconds) {
        if (milliseconds == null) {
            milliseconds = BigDecimal.ZERO;
        }
        return beforeMillisecs(ts, milliseconds.longValue());
    }

    /**
     * Gets the <code>Timestamp</code> which time is the given milliseconds after the given <code>Timestamp</code>.
     * @param ts the reference <code>Timestamp</code>
     * @param milliseconds number of milliseconds after ts for which to return
     * @return a <code>Timestamp</code>, <code>null</code> if ts is <code>null</code>
     */
    public static Timestamp afterMillisecs(Timestamp ts, long milliseconds) {
        if (ts != null) {
            return new Timestamp(ts.getTime() + milliseconds);
        } else {
            return null;
        }
    }

    /**
     * Gets the <code>Timestamp</code> which time is the given milliseconds after the given <code>Timestamp</code>.
     * @param ts the reference <code>Timestamp</code>
     * @param milliseconds number of milliseconds after ts for which to return
     * @return a <code>Timestamp</code>, <code>null</code> if ts is <code>null</code>
     */
    public static Timestamp afterMillisecs(Timestamp ts, Double milliseconds) {
        if (milliseconds == null) {
            milliseconds = new Double(0);
        }
        return afterMillisecs(ts, milliseconds.longValue());
    }

    /**
     * Converts a <code>String</code> into it corresponding <code>Timestamp</code> value.
     * @param timestampString a <code>String</code> representing a <code>Timestamp</code>
     * @return the corresponding <code>Timestamp</code> value
     * @deprecated Use UtilDate.toTimestamp(String timestampString, TimeZone timeZone, Locale locale)
     */
    @Deprecated
    public static Timestamp toTimestamp(String timestampString) {
        return UtilDate.toTimestamp(timestampString, TimeZone.getDefault(), Locale.getDefault());
    }

    /**
     * Gets a list of countries.
     * @param delegator a <code>Delegator</code> value
     * @return a <code>List</code> of countries Geo <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getCountries(Delegator delegator) throws GenericEntityException {
        return delegator.findByAndCache("Geo", UtilMisc.toMap("geoTypeId", "COUNTRY"), UtilMisc.toList("geoName"));
    }

    /**
     * Gets a list of states in the given country.
     * @param delegator a <code>Delegator</code> value
     * @param countryGeoId the country for which to return the list of states
     * @return a <code>List</code> of states Geo <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getStates(Delegator delegator, String countryGeoId)
            throws GenericEntityException {
        return delegator.findByAndCache("GeoAssocAndGeoTo",
                UtilMisc.toMap("geoIdFrom", countryGeoId, "geoAssocTypeId", "REGIONS"), UtilMisc.toList("geoName"));
    }

    /**
     * Gets a list of currencies.
     * @param delegator a <code>Delegator</code> value
     * @return a <code>List</code> of currencies <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getCurrencies(Delegator delegator) throws GenericEntityException {
        return delegator.findByAndCache("Uom", UtilMisc.toMap("uomTypeId", "CURRENCY_MEASURE"),
                UtilMisc.toList("abbreviation"));
    }

    /**
     * Gets the organization party ID.
     * @param request a <code>HttpServletRequest</code> value
     * @return the organizationPartyId from the session, or <code>null</code> if not set
     */
    public static String getOrganizationPartyId(HttpServletRequest request) {
        HttpSession session = request.getSession();
        if (session == null) {
            return null;
        }

        Boolean applicationContextSet = (Boolean) session.getAttribute("applicationContextSet");
        if (applicationContextSet == null) {
            UtilConfig.checkDefaultOrganization(request);
        }

        String org = (String) session.getAttribute("organizationPartyId");
        if (UtilValidate.isEmpty(org)) {
            return null;
        }

        return org;
    }

    /**
     * Gets the given organization corresponding PartyAcctgPreference.
     * @param organizationPartyId the organization identifier
     * @param delegator a <code>Delegator</code> value
     * @return the PartyAcctgPreference <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static GenericValue getOrgAcctgPref(String organizationPartyId, Delegator delegator)
            throws GenericEntityException {
        GenericValue orgAcctgPref = delegator.findByPrimaryKeyCache("PartyAcctgPreference",
                UtilMisc.toMap("partyId", organizationPartyId));
        return orgAcctgPref;
    }

    /**
     * Re-factored method to return a field from organizationPartyId or null if it is not set or there are problems.
     * Please define specific get methods and not use this method -- that's why I made it private (Si)
     * @param organizationPartyId the organization identifier
     * @param fieldId name of the field to return from the organization PartyAcctgPreference
     * @param delegator a <code>Delegator</code> value
     * @return the PartyAcctgPreference field string value or <code>null</code> if there is any problem
     */
    private static String getOrgAcctgPrefField(String organizationPartyId, String fieldId, Delegator delegator) {
        try {
            GenericValue acctgPref = getOrgAcctgPref(organizationPartyId, delegator);
            if (UtilValidate.isNotEmpty(acctgPref)) {
                return acctgPref.getString(fieldId);
            } else {
                Debug.logWarning("Cannot get accounting preference [" + fieldId + "] for [" + organizationPartyId
                        + "], no PartyAcctgPreference found.", MODULE);
                return null;
            }
        } catch (GenericEntityException ex) {
            Debug.logError("Problem getting accounting preference [" + fieldId + "] for [" + organizationPartyId
                    + "]: " + ex.getMessage(), MODULE);
            return null;
        }
    }

    /**
     * Gets the PartyAcctgPreference.cogsMethodId for the given organization corresponding.
     * @param organizationPartyId the organization identifier
     * @param delegator a <code>Delegator</code> value
     * @return the PartyAcctgPreference.cogsMethodId or <code>null</code> if there is any problem
     */
    public static String getOrgCOGSMethodId(String organizationPartyId, Delegator delegator) {
        return getOrgAcctgPrefField(organizationPartyId, "cogsMethodId", delegator);
    }

    /**
     * Gets the PartyAcctgPreference.baseCurrencyUomId for the given organization corresponding.
     * @param organizationPartyId the organization identifier
     * @param delegator a <code>Delegator</code> value
     * @return the PartyAcctgPreference.cogsMethodId or <code>null</code> if there is any problem
     */
    public static String getOrgBaseCurrency(String organizationPartyId, Delegator delegator) {
        return getOrgAcctgPrefField(organizationPartyId, "baseCurrencyUomId", delegator);
    }

    /**
     * Gets the <code>List</code> of currently active contactMechIds for the given facility.
     * @param facilityId the facility identifier
     * @param delegator a <code>Delegator</code> value
     * @return the <code>List</code> of currently active contactMechIds for the given facility
     * @throws GenericEntityException if an error occurs
     */
    @SuppressWarnings("unchecked")
    public static List<String> getFacilityContactMechIds(String facilityId, Delegator delegator)
            throws GenericEntityException {
        List<GenericValue> facilityContactMechs = delegator.findByAnd("FacilityContactMech",
                EntityCondition.makeCondition(EntityOperator.AND,
                        EntityCondition.makeCondition("facilityId", facilityId), EntityUtil.getFilterByDateExpr()));
        return EntityUtil.getFieldListFromEntityList(facilityContactMechs, "contactMechId", true);
    }

    /**
     * Gets the current postal address of for the given facility.
     * @param delegator a <code>Delegator</code> value
     * @param facilityId the facility identifier
     * @return the current postal address of for the given facility, <code>null</code> if none is found
     * @throws GenericEntityException if an error occurs
     */
    public static GenericValue getFacilityPostalAddress(Delegator delegator, String facilityId)
            throws GenericEntityException {
        List<GenericValue> facilityMechPurps = delegator.findByAndCache("FacilityContactMechPurpose",
                UtilMisc.toMap("facilityId", facilityId, "contactMechPurposeTypeId", "SHIP_ORIG_LOCATION"));
        facilityMechPurps = EntityUtil.filterByDate(facilityMechPurps);
        if (UtilValidate.isNotEmpty(facilityMechPurps)) {
            return EntityUtil.getFirst(facilityMechPurps).getRelatedOne("ContactMech")
                    .getRelatedOne("PostalAddress");
        }
        return null;
    }

    /**
     * Gets the <code>List</code> of facilityId of the facilities which can get inventory for the given organization.
     * @param organizationPartyId the organization identifier
     * @param delegator a <code>Delegator</code> value
     * @return the <code>List</code> of facilityId of the facilities which can get inventory for the given organization
     * @throws GenericEntityException if an error occurs
     */
    public static List<String> getOrgReceivingFacilityIds(String organizationPartyId, Delegator delegator)
            throws GenericEntityException {
        return EntityUtil.getFieldListFromEntityList(
                getOrganizationReceivingFacilities(organizationPartyId, delegator), "facilityId", true);
    }

    /**
     * Gets the <code>List</code> of facilities (warehouses) for which inventory is received on behalf of the given organization.
     * This could be either Facility owned by the organization or where the organization has a role of RECV_INV_FOR.
     * @param organizationPartyId the organization identifier
     * @param delegator a <code>Delegator</code> value
     * @return the <code>List</code> of receiving facilities
     * @throws GenericEntityException if an error occurs
     */
    public static List<GenericValue> getOrganizationReceivingFacilities(String organizationPartyId,
            Delegator delegator) throws GenericEntityException {
        if (UtilValidate.isEmpty(organizationPartyId)) {
            return null;
        }

        // first find the facilities where this organization is an owner
        List<GenericValue> facilities = delegator.findByAnd("Facility",
                UtilMisc.toMap("ownerPartyId", organizationPartyId), UtilMisc.toList("facilityName"));

        // now find the facilities where this organization can receive inventory
        // SELECT * FROM FACILITY WHERE FACILITY_ID IN (SELECT * FROM FACILITY_ROLE WHERE ROLE_TYPE_ID = 'RECV_INV_FOR' AND PARTY_ID = organizationPartyId))
        List<GenericValue> additionalFacilities = delegator.findByAnd("FacilityRole",
                UtilMisc.toMap("roleTypeId", "RECV_INV_FOR", "partyId", organizationPartyId));
        if (UtilValidate.isNotEmpty(additionalFacilities)) {
            facilities.addAll(delegator.findByAnd("Facility",
                    UtilMisc.toList(EntityCondition.makeCondition("facilityId", EntityOperator.IN,
                            EntityUtil.getFieldListFromEntityList(additionalFacilities, "facilityId", true)))));
        }
        return facilities;
    }

    /**
     * Retrieves information required for companyHeader.fo.ftl for the given organization.
     * @param organizationPartyId the organization identifier
     * @param delegator a <code>Delegator</code> value
     * @return a <code>Map</code> containing organization information
     * @exception GenericEntityException if an error occurs
     */
    public static Map<String, Object> getOrganizationHeaderInfo(String organizationPartyId, Delegator delegator)
            throws GenericEntityException {
        Map<String, Object> results = FastMap.newInstance();

        // the logo image URL
        GenericValue partyGroup = delegator.findByPrimaryKey("PartyGroup",
                UtilMisc.toMap("partyId", organizationPartyId));
        if (partyGroup != null && UtilValidate.isNotEmpty(partyGroup.getString("logoImageUrl"))) {
            results.put("organizationLogoImageUrl", partyGroup.getString("logoImageUrl"));
        }

        // the company name
        if (partyGroup != null && UtilValidate.isNotEmpty(partyGroup.getString("groupName"))) {
            results.put("organizationCompanyName", partyGroup.getString("groupName"));
        }

        // the address
        List<GenericValue> addresses = delegator.findByAnd("PartyContactMechPurpose",
                UtilMisc.toMap("partyId", organizationPartyId, "contactMechPurposeTypeId", "GENERAL_LOCATION"));
        GenericValue address = EntityUtil.getFirst(
                EntityUtil.filterByDate(addresses, UtilDateTime.nowTimestamp(), "fromDate", "thruDate", true));
        if (address != null) {
            GenericValue postalAddress = delegator.findByPrimaryKey("PostalAddress",
                    UtilMisc.toMap("contactMechId", address.getString("contactMechId")));
            if (postalAddress != null) {
                results.put("organizationPostalAddress", postalAddress);

                // get the country name and state/province abbreviation
                GenericValue country = postalAddress.getRelatedOneCache("CountryGeo");
                if (country != null) {
                    results.put("countryName", country.getString("geoName"));
                }
                GenericValue stateProvince = postalAddress.getRelatedOneCache("StateProvinceGeo");
                if (stateProvince != null) {
                    results.put("stateProvinceAbbrv", stateProvince.getString("abbreviation"));
                }
            }
        }

        return results;
    }

    /**
     * Checks if there are any valid status changes from the given statusId.
     * @param statusId the original status
     * @param delegator a <code>Delegator</code> value
     * @return a <code>boolean</code> if any valid status changes from the given statusId is found
     * @exception GenericEntityException if an error occurs
     */
    public static boolean hasValidChange(String statusId, Delegator delegator) throws GenericEntityException {
        return delegator.findCountByAnd("StatusValidChange", UtilMisc.toMap("statusId", statusId)) > 0 ? true
                : false;
    }

    /**
     * Fetches a <code>List</code> of <code>StatusValidChange</code> that are a valid change from the given status.
     * @param statusId the current status ID
     * @param delegator a <code>Delegator</code> value
     * @return the <code>List</code> of status <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getValidChanges(String statusId, Delegator delegator)
            throws GenericEntityException {
        return delegator.findByAndCache("StatusValidChange", UtilMisc.toMap("statusId", statusId));
    }

    /**
     * Checks if there is a <code>StatusValidChange</code> for the given status and status to.
     * @param statusId the current status ID
     * @param statusIdTo the new status ID
     * @param delegator a <code>Delegator</code> value
     * @return <code>true</code> if the change is valid
     * @exception GenericEntityException if an error occurs
     */
    public static boolean isValidChange(String statusId, String statusIdTo, Delegator delegator)
            throws GenericEntityException {
        return delegator.findCountByAnd("StatusValidChange",
                UtilMisc.toMap("statusId", statusId, "statusIdTo", statusIdTo)) > 0 ? true : false;
    }

    /**
     * Fetches a <code>List</code> of <code>StatusItem</code> by status type from the cache.
     * @param statusTypeId the type of status to fetch
     * @param delegator a <code>Delegator</code> value
     * @return the <code>List</code> of status <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getStatuses(String statusTypeId, Delegator delegator)
            throws GenericEntityException {
        return delegator.findByAndCache("StatusItem", UtilMisc.toMap("statusTypeId", statusTypeId),
                UtilMisc.toList("sequenceId"));
    }

    /**
     * Checks if a request has an error set.
     * @param request a <code>HttpServletRequest</code> value
     * @return a <code>Boolean</code> value
     */
    @SuppressWarnings("unchecked")
    public static Boolean hasError(HttpServletRequest request) {
        Enumeration<String> attrs = request.getAttributeNames();
        while (attrs.hasMoreElements()) {
            String a = attrs.nextElement();
            if ("_ERROR_MESSAGE_LIST_".equals(a) || "_ERROR_MESSAGE_".equals(a) || "opentapsErrors".equals(a)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets a request parameter, or <code>null</code> if the string was empty.  This simplifies the trinary logic
     * of empty strings vs <code>null</code> strings into a simple boolean, either the string exists and has content
     * or it is null.
     * @param request a <code>HttpServletRequest</code> value
     * @param parameterName the parameter name to get
     * @return the parameter <code>String</code> value or <code>null</code> if the value was <code>null</code> or empty
     * @see #getParameter(Map, String)
     * @see #getUTF8Parameter(HttpServletRequest, String)
     */
    public static String getParameter(HttpServletRequest request, String parameterName) {
        String result = request.getParameter(parameterName);
        if (result == null) {
            return null;
        }
        result = result.trim();
        if (result.length() == 0) {
            return null;
        }
        return result;
    }

    /**
     * As above, but search a downstream context.  This allows searching fields set by the screen widget,
     * by the request parameters, by the session, by the global context, and so on.
     * @param context the context <code>Map</code>
     * @param parameterName the parameter name to get
     * @return the parameter <code>String</code> value or <code>null</code> if the value was <code>null</code> or empty
     * @see #getParameter(HttpServletRequest, String)
     */
    @SuppressWarnings("unchecked")
    public static String getParameter(Map<String, ?> context, String parameterName) {
        String result = (String) context.get(parameterName); // search the context map
        if (result == null) {
            // search the request and session using the special "parameters" map
            Map<String, String> parameters = (Map<String, String>) context.get("parameters");
            if (parameters != null) {
                result = parameters.get(parameterName);
            }
            if (result == null) {
                // search the global context
                Map<String, String> global = (Map<String, String>) context.get("globalContext");
                if (global != null) {
                    result = global.get(parameterName);
                }
            }
        }
        if (result == null) {
            return null;
        }
        result = result.trim();
        if (result.length() == 0) {
            return null;
        }
        return result;
    }

    /**
     * Gets an UTF8 encoded string from request parameters.
     * @param request a <code>HttpServletRequest</code> value
     * @param parameterName the parameter name to get
     * @return a <code>String</code> value
     * @see #getParameter(HttpServletRequest, String)
     */
    public static String getUTF8Parameter(HttpServletRequest request, String parameterName) {
        try {
            String result = request.getParameter(parameterName);
            if (result == null) {
                return null;
            }
            result = new String(result.getBytes("iso-8859-1"), "UTF-8");
            if (result == null) {
                return null;
            }
            result = result.trim();
            if (result.length() == 0) {
                return null;
            }
            return result;
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * Gets the <code>List</code> of children for the given parent.  The entity must have a Parent/Child relationship.
     * This function relies on the cache and is intended for data that rarely changes, such as type entities.
     * @param delegator a <code>Delegator</code> value
     * @param parent the parent <code>GenericValue</code>
     * @return the <code>List</code> of children <code>GenericValue</code> for the given parent
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getEntityChildren(Delegator delegator, GenericValue parent)
            throws GenericEntityException {
        List<GenericValue> combined = FastList.newInstance();
        if (parent == null) {
            return combined;
        }
        combined.add(parent);
        List<GenericValue> children = parent.getRelatedCache("Child" + parent.getEntityName());
        for (Iterator<GenericValue> iter = children.iterator(); iter.hasNext();) {
            GenericValue child = iter.next();
            combined.addAll(getEntityChildren(delegator, child));
        }
        return combined;
    }

    /**
     * Given a parent entity, returns an entity expression that constrains to the members of the parent's family.
     * This function relies on the cache and is intended for data that rarely changes, such as type entities.
     * It also assumes that the entity has one primary key, whose field name must be specified by pkFieldName.
     * @param delegator a <code>Delegator</code> value
     * @param entityName the entity name
     * @param pkFieldName the field name of the primary key
     * @param parentId the value of the primary key for the parent (root) entity
     * @return an <code>EntityExpr</code> value
     * @exception GenericEntityException if an error occurs
     */
    public static EntityExpr getEntityChildrenExpr(Delegator delegator, String entityName, String pkFieldName,
            String parentId) throws GenericEntityException {
        return getEntityChildrenExpr(delegator, entityName, pkFieldName, parentId, false);
    }

    /**
     * Given a parent entity, returns an entity expression that constrains to the members NOT in the parent's family.
     * This function relies on the cache and is intended for data that rarely changes, such as type entities.
     * It also assumes that the entity has one primary key, whose field name must be specified by pkFieldName.
     * @param delegator a <code>Delegator</code> value
     * @param entityName the entity name
     * @param pkFieldName the field name of the primary key
     * @param parentId the value of the primary key for the parent (root) entity
     * @return an <code>EntityExpr</code> value
     * @exception GenericEntityException if an error occurs
     */
    public static EntityExpr getEntityChildrenComplementExpr(Delegator delegator, String entityName,
            String pkFieldName, String parentId) throws GenericEntityException {
        return getEntityChildrenExpr(delegator, entityName, pkFieldName, parentId, true);
    }

    /**
     * Builds an <code>EntityExpr</code> for getting parent / child related entities.
     * See above.
     * @param delegator a <code>Delegator</code> value
     * @param entityName the entity name
     * @param pkFieldName the field name of the primary key
     * @param parentId the value of the primary key for the parent (root) entity
     * @param isComplement if <code>true</code>, gets the child not related to the parent, else get the child related to the parent
     * @return an <code>EntityExpr</code> value
     * @exception GenericEntityException if an error occurs
     */
    private static EntityExpr getEntityChildrenExpr(Delegator delegator, String entityName, String pkFieldName,
            String parentId, boolean isComplement) throws GenericEntityException {

        // first get the root value and if it doesn't exist, return a condition that always evaluates to true
        GenericValue parent = delegator.findByPrimaryKeyCache(entityName, UtilMisc.toMap(pkFieldName, parentId));
        if (parent == null) {
            Debug.logWarning("Cannot find " + entityName + " [" + parentId + "]", MODULE);
            return EntityCondition.makeCondition("1", EntityOperator.EQUALS, "1");
        }

        // recursively build the list of ids that are of this type
        Set<Object> ids = FastSet.newInstance();
        recurseGetEntityChildrenSet(parent, ids, pkFieldName);

        // make a WHERE paymentTypeId IN (list of ids) expression or NOT_IN for complement search
        return EntityCondition.makeCondition(pkFieldName,
                (isComplement ? EntityOperator.NOT_IN : EntityOperator.IN), ids);
    }

    /**
     * Builds a set of id values for a parent/child tree using pkFieldName.
     * @param parent the parent <code>GenericValue</code>
     * @param ids the <code>Set</code> of identifiers to populate
     * @param pkFieldName the name of the primary key field in the entity
     * @exception GenericEntityException if an error occurs
     */
    private static void recurseGetEntityChildrenSet(GenericValue parent, Set<Object> ids, String pkFieldName)
            throws GenericEntityException {
        ids.add(parent.get(pkFieldName));
        List<GenericValue> children = parent.getRelatedCache("Child" + parent.getEntityName());
        for (Iterator<GenericValue> iter = children.iterator(); iter.hasNext();) {
            GenericValue child = iter.next();
            recurseGetEntityChildrenSet(child, ids, pkFieldName);
        }
    }

    /**
     * Checks if an application component is loaded.
     * @param componentName the application component name
     * @return <code>true</code> if the application component is loaded
     */
    public static boolean isLoaded(String componentName) {
        if (UtilValidate.isEmpty(componentName)) {
            return false;
        }
        Collection<ComponentConfig> componentConfigs = ComponentConfig.getAllComponents();
        if (componentConfigs == null) {
            return false;
        }
        for (ComponentConfig componentConfig : componentConfigs) {
            if (componentName.equalsIgnoreCase(componentConfig.getComponentName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Makes a query string from the parameters map. Used for navigation history.
     * @param parameters the parameters <code>Map</code>
     * @param override a <code>List</code> of parameter names to include, if <code>null</code> or empty then all the parameters will be included except a few common
     * @return a query <code>String</code> such as <code>paramName1=value1&amp;paramName2=value2 ...</code>
     */
    public static String makeHistoryQueryString(Map<String, ? extends Object> parameters, List<String> override) {
        Set<String> keySet = parameters.keySet();
        Set<String> excludes = FastSet.<String>newInstance();
        String queryString = "";

        if (override != null && override.size() > 0) {
            // Include to query string parameters with names that is listed in override
            String[] tokens = UtilHttp.urlEncodeArgs(parameters, false).split("[?&]");
            for (String key : override) {
                for (String token : tokens) {
                    if (token.startsWith(key)) {
                        queryString += (token + "&");
                    }
                }
            }
        } else {
            // Exclude from query string well known and unnecessary in our case parameters
            excludes.add("VIEW_INDEX");
            excludes.add("VIEW_SIZE");
            excludes.add("viewIndex");
            excludes.add("viewSize");
            excludes.add("dispatcher");
            excludes.add("thisRequestUri");
            excludes.add("servletContext");
            excludes.add("delegator");
            excludes.add("autoUserLogin");
            excludes.add("userLogin");
            excludes.add("person");
            excludes.add("delegatorName");
            excludes.add("visit");
            excludes.add("entityDelegatorName");
            excludes.add("localDispatcherName");
            excludes.add("componentName");
            excludes.add("autoName");
            excludes.add("applicationContextSet");
            excludes.add("multiPartMap");
            excludes.add("serviceReaderUrls");
            excludes.add("mainApplicationDecoratorLocation");
            excludes.add("mainDecoratorLocation");
            excludes.add("security");
            excludes.add("targetRequestUri");
            excludes.add("jpublishWrapper");
            excludes.add("externalLoginKey");
            excludes.add("sessionId");
            excludes.add("timeZone");
            excludes.add("ofbizServerName");

            for (String key : keySet) {

                //Exclude from query string all parameters started with "_"
                if (key.startsWith("_")) {
                    excludes.add(key);
                    continue;
                }

                //Exclude from query string keys if they are classes
                if (key.startsWith("org.") || key.startsWith("java.") || key.startsWith("javax.")
                        || key.startsWith("javolution.") || key.startsWith("paginator.")) {
                    excludes.add(key);
                    continue;
                }

                // Exclude permission variables as well
                if (key.startsWith("has") && key.endsWith("Permission")) {
                    excludes.add(key);
                    continue;
                }
            }

            queryString = UtilHttp.stripNamedParamsFromQueryString(UtilHttp.urlEncodeArgs(parameters, false),
                    excludes);
        }

        int lastIndex = queryString.length() - 1;
        if (UtilValidate.isNotEmpty(queryString) && '&' == queryString.charAt(lastIndex)) {
            queryString = queryString.substring(0, lastIndex);
        }

        return queryString;
    }

    /**
     * Prepares an History entry for later processing.
     * @param text the text that should be displayed as the label of the entry
     * @param view the view name, if not <code>null</code> the history entry will only get stored if this matches current request URI
     * @param override a <code>List</code> of parameter names to include, if <code>null</code> or empty then all the parameters will be included except a few common
     * @return the history entry <code>Map</code>
     */
    public static Map<String, ?> makeHistoryEntry(String text, String view, List<String> override) {
        Map<String, Object> entry = FastMap.<String, Object>newInstance();
        if (text == null) {
            throw new IllegalArgumentException("Argument \"text\" can't be null");
        }
        entry.put("text", text);
        if (view != null) {
            entry.put("view", view);
        }
        if (override != null) {
            entry.put("override", override);
        }
        return entry;
    }

    /**
     * Prepares an History entry for later processing.
     * @param text the text that should be displayed as the label of the entry
     * @param view the view name
     * @return the history entry <code>Map</code>
     */
    public static Map<String, ?> makeHistoryEntry(String text, String view) {
        return makeHistoryEntry(text, view, null);
    }

    /**
     * Prepares an History entry for later processing.
     * @param text the text that should be displayed as the label of the entry
     * @return the history entry <code>Map</code>
     */
    public static Map<String, ?> makeHistoryEntry(String text) {
        return makeHistoryEntry(text, null, null);
    }

    /**
     * Given a set of values, calculates the correspondent % of total.
     *
     * @param values a <code>Map</code> of values, such as customer/vendor balance values
     * @param minPercentage the minimum percentage to consider for calculation purposes
     * @param locale the <code>Locale</code> used to build the label strings
     * @return returns the weight (percentage) of each balance
     */
    public static List<Map<String, Number>> getPercentageValues(Map<String, BigDecimal> values,
            BigDecimal minPercentage, Locale locale) {

        Collection<BigDecimal> inValues = values.values();
        Set<String> keys = values.keySet();
        List<Map<String, Number>> list = new LinkedList<Map<String, Number>>();
        BigDecimal total = BigDecimal.ZERO;
        BigDecimal othersTotal = BigDecimal.ZERO;
        final int decimals = 2; // precision for the percentage values

        // total up all the values
        for (BigDecimal value : inValues) {
            total = total.add(value);
        }

        if (total.signum() > 0) { //prevent division by zero
            for (String key : keys) {
                BigDecimal value = values.get(key);
                value = value.divide(total, 10, RoundingMode.HALF_UP);
                if (value.compareTo(minPercentage) == 1) { //greater than minPercentage?
                    Map<String, Number> map = FastMap.newInstance();
                    value = value.multiply(new BigDecimal(100)).setScale(decimals, RoundingMode.HALF_UP); //display only 2 decimal places
                    map.put(key, value);
                    list.add(map);
                } else {
                    othersTotal = othersTotal.add(value).setScale(decimals + 3, RoundingMode.HALF_UP);
                }
            }

            // normalize to % - ie 0.577 to 57.7
            othersTotal = othersTotal.multiply(new BigDecimal(100)).setScale(decimals, RoundingMode.HALF_UP);
            if (othersTotal.signum() > 0) {
                list.add(UtilMisc.<String, Number>toMap(UtilMessage.expandLabel("CommonOther", locale)
                        + String.format(" (%1$s%%)", othersTotal.toString()), othersTotal));
            }
        }

        return list;
    }

    /**
     * Reads a simply formatted, single sheet Excel document into a list of <code>Map</code> skipping the first row considered to be the header.
     * @param stream an <code>InputStream</code> of the excel document
     * @param columnNames a List containing the keys to use when mapping the columns into the Map (column 1 goes in the Map key columnNames 1, etc ...)
     * @return the List of Map representing the rows
     * @throws IOException if an error occurs
     */
    public static List<Map<String, String>> readExcelFile(InputStream stream, List<String> columnNames)
            throws IOException {
        return readExcelFile(stream, columnNames, 1);
    }

    /**
     * Reads a simply formatted, single sheet Excel document into a list of <code>Map</code>.
     * @param stream an <code>InputStream</code> of the excel document
     * @param columnNames a List containing the keys to use when mapping the columns into the Map (column 1 goes in the Map key columnNames 1, etc ...)
     * @param skipRows number of rows to skip, typically 1 to skip the header row
     * @return the List of Map representing the rows
     * @throws IOException if an error occurs
     */
    public static List<Map<String, String>> readExcelFile(InputStream stream, List<String> columnNames,
            int skipRows) throws IOException {
        POIFSFileSystem fs = new POIFSFileSystem(stream);
        HSSFWorkbook wb = new HSSFWorkbook(fs);
        HSSFSheet sheet = wb.getSheetAt(0);
        int sheetLastRowNumber = sheet.getLastRowNum();
        List<Map<String, String>> rows = new ArrayList<Map<String, String>>();
        for (int j = skipRows; j <= sheetLastRowNumber; j++) {
            HSSFRow erow = sheet.getRow(j);
            Map<String, String> row = new HashMap<String, String>();
            for (int i = 0; i < columnNames.size(); i++) {
                String columnName = columnNames.get(i);
                HSSFCell cell = erow.getCell(i);
                String s = "";
                if (cell != null) {

                    // check if cell contains a number
                    BigDecimal bd = null;
                    try {
                        double d = cell.getNumericCellValue();
                        bd = BigDecimal.valueOf(d);
                    } catch (Exception e) {
                        // do nothing
                    }
                    if (bd == null) {
                        s = cell.toString().trim();
                    } else {
                        // if cell contains number trim the tailing zeros so that for example postal code string
                        // does not appear as a floating point number
                        s = bd.toPlainString();
                        // convert XX.XX000 into XX.XX
                        s = s.replaceFirst("^(-?\\d+\\.0*[^0]+)0*\\s*$", "$1");
                        // convert XX.000 into XX
                        s = s.replaceFirst("^(-?\\d+)\\.0*$", "$1");
                    }
                }
                Debug.logInfo("readExcelFile cell (" + j + ", " + i + ") as (" + columnName + ") == " + s, MODULE);
                row.put(columnName, s);
            }
            rows.add(row);
        }
        return rows;
    }

    /**
     * Creates an Excel document with a given column name list, and column data list.
     * The String objects in the column name list are used as Map keys to look up the corresponding
     * column header and data. The column data to be exported is a List of Map objects where
     * the first Map element contains column headers, and the rest has all the column data.
     * @param workBookName a String object as Excel file name
     * @param workSheetName a String object as the name of the Excel sheet
     * @param columnNameList a List of String objects as column names, they usually correspond to entity field names
     * @param data a List of Map objects to be exported where the first Map element contains column headers,
     *        and the rest has all the column data.
     * @throws IOException if an error occurs
     */
    public static void saveToExcel(final String workBookName, final String workSheetName,
            final List<String> columnNameList, final List<Map<String, Object>> data) throws IOException {
        if (StringUtils.isEmpty(workBookName)) {
            throw new IllegalArgumentException("Argument workBookName can't be empty");
        }

        if (StringUtils.isEmpty(workSheetName)) {
            throw new IllegalArgumentException("Argument workSheetName can't be empty");
        }

        if (columnNameList == null || columnNameList.isEmpty()) {
            throw new IllegalArgumentException("Argument columnNameList can't be empty");
        }

        // the data list should have at least one element for the column headers
        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("Argument data can't be empty");
        }

        FileOutputStream fileOut = new FileOutputStream(new File(workBookName));
        assert fileOut != null;

        HSSFWorkbook workBook = new HSSFWorkbook();
        assert workBook != null;

        HSSFSheet workSheet = workBook.createSheet(workSheetName);
        assert workSheet != null;

        // create the header row

        HSSFRow headerRow = workSheet.createRow(0);
        assert workSheet != null;

        HSSFFont headerFont = workBook.createFont();
        assert headerFont != null;

        headerFont.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
        headerFont.setColor(HSSFColor.BLACK.index);

        HSSFCellStyle headerCellStyle = workBook.createCellStyle();
        assert headerCellStyle != null;

        headerCellStyle.setFont(headerFont);

        // the first data list element should always be the column header map
        Map<String, Object> columnHeaderMap = data.get(0);

        if (columnHeaderMap != null) {
            for (short i = 0; i < columnNameList.size(); i++) {
                HSSFCell cell = headerRow.createCell(i);
                assert cell != null;

                cell.setCellStyle(headerCellStyle);

                Object columnHeaderTitle = columnHeaderMap.get(columnNameList.get(i));
                if (columnHeaderTitle != null) {
                    cell.setCellValue(new HSSFRichTextString(columnHeaderTitle.toString()));
                }
            }
        }

        // create data rows

        // column data starts from the second element
        if (data.size() > 1) {

            // Create the style used for dates.
            HSSFCellStyle dateCellStyle = workBook.createCellStyle();
            String dateFormat = "mm/dd/yyyy hh:mm:ss";
            HSSFDataFormat hsfDateFormat = workBook.createDataFormat();
            short dateFormatIdx = hsfDateFormat.getFormat(dateFormat);
            if (dateFormatIdx == -1) {
                Debug.logWarning("Date format [" + dateFormat
                        + "] could be found or created, try one of the pre-built instead:"
                        + HSSFDataFormat.getBuiltinFormats(), MODULE);
            }
            dateCellStyle.setDataFormat(dateFormatIdx);

            for (int dataRowIndex = 1; dataRowIndex < data.size(); dataRowIndex++) {
                Map<String, Object> rowDataMap = data.get(dataRowIndex);
                if (rowDataMap == null) {
                    continue;
                }

                HSSFRow dataRow = workSheet.createRow(dataRowIndex);
                assert dataRow != null;

                for (short i = 0; i < columnNameList.size(); i++) {
                    HSSFCell cell = dataRow.createCell(i);
                    assert cell != null;

                    Object cellData = rowDataMap.get(columnNameList.get(i));
                    if (cellData != null) {
                        // Note: dates are actually numeric values in Excel and so the cell need to have
                        //  a special style set so it actually displays as a date
                        if (cellData instanceof Calendar) {
                            cell.setCellStyle(dateCellStyle);
                            cell.setCellValue((Calendar) cellData);
                        } else if (cellData instanceof Date) {
                            cell.setCellStyle(dateCellStyle);
                            cell.setCellValue((Date) cellData);
                        } else if (cellData instanceof BigDecimal) {
                            cell.setCellValue(((BigDecimal) cellData).doubleValue());
                        } else if (cellData instanceof Double) {
                            cell.setCellValue(((Double) cellData).doubleValue());
                        } else if (cellData instanceof Integer) {
                            cell.setCellValue(((Integer) cellData).doubleValue());
                        } else if (cellData instanceof BigInteger) {
                            cell.setCellValue(((BigInteger) cellData).doubleValue());
                        } else {
                            cell.setCellValue(new HSSFRichTextString(cellData.toString()));
                        }
                    }
                }
            }
        }

        // auto size the column width
        if (columnHeaderMap != null) {
            for (short i = 0; i < columnNameList.size(); i++) {
                workSheet.autoSizeColumn(i);
            }
        }

        // create the Excel file
        workBook.write(fileOut);
        fileOut.close();
    }

    /**
     * Gets the Url context help resource from the opentaps.helpUrlPattern
     * and the ContextHelpResource entity if it exits. Return null if not.
     * @param delegator a <code>Delegator</code> value
     * @param appName the application name (eg: crmsfa, financials ...)
     * @param screenName the name of the screen
     * @param screenState the state of the screen, optional
     * @return an <code>URL</code> value
     */
    @SuppressWarnings("unchecked")
    public static URL getUrlContextHelpResource(Delegator delegator, String appName, String screenName,
            String screenState) {
        List helps = null;
        URL helpUrl = null;

        try {
            if (UtilValidate.isEmpty(screenState)) {
                helps = delegator.findByAndCache("ContextHelpResource",
                        UtilMisc.toMap("screenName", screenName, "applicationName", appName));
            } else {
                helps = delegator.findByAndCache("ContextHelpResource", UtilMisc.toMap("screenName", screenName,
                        "applicationName", appName, "screenState", screenState));
            }
        } catch (GenericEntityException ex) {
            helps = null;
            Debug.logError("Problem getting context help resource for [" + appName + ", " + screenName + "]: "
                    + ex.getMessage(), MODULE);
        }
        GenericValue help = EntityUtil.getFirst(helps);

        if (help == null) {
            return helpUrl;
        }

        Map args = UtilMisc.toMap("remotePageName", help.getString("remotePageName"), "remotePageVersion",
                help.getString("remotePageVersion"));
        String helpUrlPattern = help.getString("overrideUrlPattern");

        if (UtilValidate.isEmpty(helpUrlPattern)) {
            helpUrlPattern = UtilConfig.getPropertyValue("opentaps", "opentaps.helpUrlPattern");
        }

        if (UtilValidate.isEmpty(helpUrlPattern)) {
            UtilMessage.logServiceWarning("OpentapsError_PropertyNotConfigured",
                    UtilMisc.toMap("propertyName", "opentaps.helpUrlPattern", "resource", "opentaps.properties"),
                    UtilMisc.ensureLocale(null), MODULE);
            return null;
        }

        try {
            helpUrl = new URL(FlexibleStringExpander.expandString(helpUrlPattern, args));
        } catch (MalformedURLException ex) {
            Debug.logError("Problem formatting context url help resource for [" + appName + ", " + screenName
                    + "]: " + ex.getMessage(), MODULE);
        }
        return helpUrl;
    }

    /**
     * Converts a localized number <code>String</code> to a <code>BigDecimal</code> using the given <code>Locale</code>, defaulting to the system <code>Locale</code>.
     * @param locale the <code>Locale</code> to use for parsing the number, optional, defaults to the system <code>Locale</code>
     * @param numberString a <code>String</code> to convert to a <code>BigDecimal</code>
     * @return the corresponding <code>BigDecimal</code> value
     * @throws ParseException if an occurs during parsing
     */
    public static BigDecimal parseLocalizedNumber(Locale locale, String numberString) throws ParseException {
        locale = UtilMisc.ensureLocale(locale);
        NumberFormat parser = NumberFormat.getNumberInstance(locale);
        Number n = parser.parse(numberString);
        return new BigDecimal(n.toString());
    }

    /**
     * Converts a <code>Number</code> into a <code>BigDecimal</code>.
     * Note that with Java 5, you can pass in a primitive, which will be autoboxed.
     * @param number the <code>Number</code> to convert
     * @return a <code>BigDecimal</code> value, or <code>null</code> if the given number was <code>null</code>
     * @exception NumberFormatException if the <code>Number</code> cannot be parsed, which shouldn't happen
     */
    public static BigDecimal asBigDecimal(Number number) throws NumberFormatException {
        if (number == null) {
            return null;
        }
        if (number instanceof BigDecimal) {
            return (BigDecimal) number;
        }
        return new BigDecimal(number.toString());
    }

    /**
     * Converts a <code>String</code> into a <code>BigDecimal</code>.
     * @param number the <code>String</code> to convert
     * @return a <code>BigDecimal</code> value, or <code>null</code> if the given number was <code>null</code>
     * @throws NumberFormatException if the <code>String</code> cannot be parsed into <code>BigDecimal</code>
     */
    public static BigDecimal asBigDecimal(String number) throws NumberFormatException {
        if (number == null) {
            return null;
        }
        return new BigDecimal(number);
    }

    /**
     * Converts an <code>Object</code> to a <code>BigDecimal</code>, provided it is a <code>String</code> or <code>Number</code>.
     * @param obj the <code>Object</code> to convert, must be a <code>String</code> or <code>Number</code>
     * @return a <code>BigDecimal</code> value, or <code>null</code> if the given number was <code>null</code>
     * @throws IllegalArgumentException if the given <code>Object</code> is not a <code>String</code> or a <code>Number</code>
     */
    public static BigDecimal asBigDecimal(Object obj) throws IllegalArgumentException {
        if (obj == null) {
            return null;
        }
        if (obj instanceof String) {
            return asBigDecimal((String) obj);
        }
        if (obj instanceof Number) {
            return asBigDecimal((Number) obj);
        }
        throw new IllegalArgumentException(
                "Cannot convert object of type [" + obj.getClass().getName() + "] to BigDecimal.");
    }

    /**
     * Sums all numeric values in the given <code>Map</code> and ignores non numbers.
     * @param map a <code>Map</code> value
     * @return the total of all the numeric values contained in the given <code>Map</code>
     */
    @SuppressWarnings("unchecked")
    public static BigDecimal mapSum(Map map) {
        BigDecimal sum = BigDecimal.ZERO;
        for (Object obj : map.values()) {
            if (obj instanceof Number) {
                sum = sum.add(asBigDecimal((Number) obj));
            }
        }
        return sum;
    }

    /**
     * This method can be helpful writing aspects as <code>proceed()</code> requires array of <code>Object</code> in arguments.
     * @param dctx a <code>DispatchContext</code> value
     * @param context a <code>Map</code> value
     * @return an <code>Object[]</code> value as <code>[DispatchContext, context]</code>
     */
    public static Object[] serviceArgsToArray(DispatchContext dctx, Map<String, Object> context) {
        Object[] params = new Object[2];
        params[0] = dctx;
        params[1] = context;
        return params;
    }

    /**
     * Intended to prepare service context because used service results may have internal
     * attributes or may not. Helpful in AspectJ advises when we emulate service ECA.
     *
     * @param dctx a <code>DispatchContext</code> value
     * @param results results from previous service call.
     * @param system set to <code>true</code> to use the system account instead of the context UserLogin
     * @return the service context <code>Map</code>
     * @throws GenericEntityException if an error occurs
     */
    public static Map<String, Object> makeValidSECAContext(DispatchContext dctx, Map<String, Object> results,
            boolean system) throws GenericEntityException {
        Map<String, Object> context = FastMap.newInstance();
        context.putAll(results);
        if (UtilValidate.isEmpty(context.get("userLogin"))) {
            if (system) {
                Delegator delegator = dctx.getDelegator();
                GenericValue systemUser = delegator.findByPrimaryKeyCache("UserLogin",
                        UtilMisc.toMap("userLoginId", "system"));
                context.put("userLogin", systemUser);
            } else {
                context.put("userLogin", context.get("userLogin"));
            }
        }
        if (UtilValidate.isEmpty(context.get("locale"))) {
            context.put("locale", context.get("locale"));
        }
        if (UtilValidate.isEmpty(context.get("timeZone"))) {
            context.put("timeZone", context.get("timeZone"));
        }
        return context;
    }

    /**
     * Fetches a <code>List</code> of Enumerations by enumTypeId from the cache.
     * @param enumTypeId the type of enumeration to fetch
     * @param delegator a <code>Delegator</code> value
     * @return the <code>List</code> of enumeration <code>GenericValue</code>
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getEnumerations(String enumTypeId, Delegator delegator)
            throws GenericEntityException {
        return delegator.findByAndCache("Enumeration", UtilMisc.toMap("enumTypeId", enumTypeId),
                UtilMisc.toList("sequenceId"));
    }

    /**
     * Fetches a localized Enumeration description from the cache.
     * @param enumId the Enumeration identifier
     * @param locale the <code>Locale</code> to use for translating the description
     * @param delegator a <code>Delegator</code> value
     * @return the localized Enumeration description, or empty an <code>String</code> if the enumeration was not found
     * @exception GenericEntityException if an error occurs
     */
    public static String getEnumerationDescription(String enumId, Locale locale, Delegator delegator)
            throws GenericEntityException {
        GenericValue e = delegator.findByPrimaryKeyCache("Enumeration", UtilMisc.toMap("enumId", enumId));
        if (e == null) {
            Debug.logWarning("Cannot find Enumeration with ID [" + enumId + "]", MODULE);
            return "";
        }
        return (String) e.get("description", locale);
    }

    /**
     * Fetches a localized Geo name from the cache.
     * @param geoId the Geo identifier
     * @param locale the <code>Locale</code> to use for translating the description
     * @param delegator a <code>Delegator</code> value
     * @return the localized Geo name, or empty an <code>String</code> if the enumeration was not found
     * @exception GenericEntityException if an error occurs
     */
    public static String getGeoName(String geoId, Locale locale, Delegator delegator)
            throws GenericEntityException {
        GenericValue e = delegator.findByPrimaryKeyCache("Geo", UtilMisc.toMap("geoId", geoId));
        if (e == null) {
            Debug.logWarning("Cannot find Geo with ID [" + geoId + "]", MODULE);
            return "";
        }
        return (String) e.get("geoName", locale);
    }

    /**
     * Fetches a Geo code from the cache.
     * @param geoId the Geo identifier
     * @param locale the <code>Locale</code>, unused at the moment
     * @param delegator a <code>Delegator</code> value
     * @return the Geo code, or empty an <code>String</code> if the enumeration was not found
     * @exception GenericEntityException if an error occurs
     */
    public static String getGeoCode(String geoId, Locale locale, Delegator delegator)
            throws GenericEntityException {
        GenericValue e = delegator.findByPrimaryKeyCache("Geo", UtilMisc.toMap("geoId", geoId));
        if (e == null) {
            Debug.logWarning("Cannot find Geo with ID [" + geoId + "]", MODULE);
            return "";
        }
        return (String) e.get("geoCode");
    }

    /**
     * Parse a comma-delimited string of email addresses and validate each.
     * @param emailAddressString comma-delimited string of email addresses
     * @return <code>Set</code> of valid email addresses
     */
    public static Set<String> getValidEmailAddressesFromString(String emailAddressString) {
        return getValidEmailAddressesFromString(emailAddressString, false);
    }

    /**
     * Parse a comma-delimited string of email addresses and validate each.
     * @param emailAddressString comma-delimited string of email addresses
     * @param requireDot if a dot is required in the email address to consider it valid
     * @return <code>Set</code> of valid email addresses
     * @deprecated <code>UtilValidate</code> removed requireDot, so this parameter is now ignored
     */
    @Deprecated
    public static Set<String> getValidEmailAddressesFromString(String emailAddressString, boolean requireDot) {
        Set<String> emailAddresses = new TreeSet<String>();
        if (UtilValidate.isNotEmpty(emailAddressString)) {
            String[] emails = emailAddressString.split(",");
            for (int x = 0; x < emails.length; x++) {
                if (!UtilValidate.isEmail(emails[x])) {
                    Debug.logInfo("Ignoring invalid email address: " + emails[x], MODULE);
                    continue;
                }
                emailAddresses.add(UtilValidate.stripWhitespace(emails[x]));
            }
        }
        return emailAddresses;
    }

    /**
     * Gets the UserLogin <code>GenericValue</code> from the request.
     * @param request a <code>HttpServletRequest</code> value
     * @return a <code>GenericValue</code> value
     */
    public static GenericValue getUserLogin(HttpServletRequest request) {
        HttpSession session = request.getSession();
        return (GenericValue) session.getAttribute("userLogin");
    }

    /**
     * Gets a <code>UserLoginViewPreference</code> value from the request.
     * @param request a <code>HttpServletRequest</code> value
     * @param applicationName the application to get the preference for
     * @param screenName the screen to get the preference for
     * @param option the option to get the value for
     * @return the option value, or <code>null</code> if not found
     * @throws GenericEntityException if an error occurs
     */
    public static String getUserLoginViewPreference(HttpServletRequest request, String applicationName,
            String screenName, String option) throws GenericEntityException {
        GenericValue userLogin = getUserLogin(request);
        Delegator delegator = (Delegator) request.getAttribute("delegator");
        GenericValue pref = delegator.findByPrimaryKeyCache("UserLoginViewPreference",
                UtilMisc.toMap("userLoginId", userLogin.get("userLoginId"), "applicationName", applicationName,
                        "screenName", screenName, "preferenceName", option));
        if (pref == null) {
            return null;
        }
        return pref.getString("preferenceValue");
    }

    /**
     * Sets a <code>UserLoginViewPreference</code> value.
     * @param request a <code>HttpServletRequest</code> value
     * @param applicationName the application to set the preference for
     * @param screenName the screen to set the preference for
     * @param option the option to set the value for
     * @param value the option value to set
     * @throws GenericEntityException if an error occurs
     */
    public static void setUserLoginViewPreference(HttpServletRequest request, String applicationName,
            String screenName, String option, String value) throws GenericEntityException {
        GenericValue userLogin = getUserLogin(request);
        Delegator delegator = (Delegator) request.getAttribute("delegator");
        GenericValue pref = delegator.findByPrimaryKey("UserLoginViewPreference",
                UtilMisc.toMap("userLoginId", userLogin.get("userLoginId"), "applicationName", applicationName,
                        "screenName", screenName, "preferenceName", option));
        if (pref == null) {
            pref = delegator.makeValue("UserLoginViewPreference",
                    UtilMisc.toMap("userLoginId", userLogin.get("userLoginId"), "applicationName", applicationName,
                            "screenName", screenName, "preferenceName", option));
        }
        pref.set("preferenceValue", value);
        delegator.createOrStore(pref);
    }

    /**
     * put a GWT Script into context.  scriptLocation here is the location of a GWT script, such as
     * "commongwt/org.opentaps.gwt.common.asterisk.asterisk" for the asterisk script in the commongwt webapp/ in
     * opentaps/opentaps-common.  This is used in lieu of
     * <set field="gwtScripts[]" value="commongwt/org.opentaps.gwt.common.asterisk.asterisk" global="true"/> in
     * a screen XML definition.
     * @param context a <code>Map</code> value
     * @param scriptLocation a <code>String</code> value
     */
    @SuppressWarnings("unchecked")
    public static void addGwtScript(Map context, String scriptLocation) {
        List<String> gwtScripts = (List<String>) context.get("gwtScripts");
        if (gwtScripts == null) {
            gwtScripts = UtilMisc.toList(scriptLocation);
            context.put("gwtScripts", gwtScripts);
        } else {
            gwtScripts.add(scriptLocation);
        }
    }

    /**
     * Get the shortcuts available in the given context.
     * Since multiple shortcuts can be applied to different actions according to the context, only the most specific
     *  actions applicable for the given context are returned. There are no two entities returned with the same "shortcut".
     * @param userLogin the current user
     * @param applicationName the current application name (eg: crmsfa, financials, ...)
     * @param screenName the screen name, which is from the URI
     * @param delegator a <code>Delegator</code> value
     * @return a <code>List</code> of <code>KeyboardShortcut</code> entities that should be activated
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getKeyboardShortcuts(GenericValue userLogin, String applicationName,
            String screenName, Delegator delegator) throws GenericEntityException {
        String userLoginId = null;
        if (userLogin == null) {
            return new ArrayList<GenericValue>();
        }
        userLoginId = userLogin.getString("userLoginId");

        List<GenericValue> shortcuts = delegator
                .findByAnd("KeyboardShortcutAndHandler",
                        UtilMisc.toList(
                                EntityCondition.makeCondition(EntityOperator.OR,
                                        EntityCondition.makeCondition("userLoginId", EntityOperator.EQUALS,
                                                userLoginId),
                                        EntityCondition.makeCondition("userLoginId", EntityOperator.EQUALS, null)),
                                EntityCondition.makeCondition(EntityOperator.OR,
                                        EntityCondition.makeCondition("applicationName", applicationName),
                                        EntityCondition.makeCondition("applicationName", null)),
                                EntityCondition.makeCondition(EntityOperator.OR,
                                        EntityCondition.makeCondition("screenName", screenName),
                                        EntityCondition.makeCondition("screenName", null))),
                        UtilMisc.toList("shortcut", "userLoginId", "screenName", "applicationName")); // nulls are returned last

        // remove global shortcuts masked / overridden by more specific ones
        Iterator<GenericValue> it = shortcuts.iterator();
        String lastShortcut = null;
        while (it.hasNext()) {
            GenericValue s = it.next();
            if (lastShortcut != null && lastShortcut.equalsIgnoreCase(s.getString("shortcut"))) {
                Debug.logWarning("Ignored masked shortcut: [" + lastShortcut + "] with action ["
                        + s.getString("actionTypeId") + " -> " + s.getString("actionTarget") + "] in screen "
                        + applicationName + "/" + screenName + ", for user [" + userLogin.getString("userLoginId")
                        + "]", MODULE);
                it.remove();
            }
            lastShortcut = s.getString("shortcut");
        }

        return shortcuts;
    }

    /**
     * Get the shortcuts available for Login page.
     * @param applicationName the current application name (eg: crmsfa, financials, ...)
     * @param screenName the screen name, which is from the URI
     * @param delegator a <code>Delegator</code> value
     * @return a <code>List</code> of <code>KeyboardShortcut</code> entities that should be activated
     * @exception GenericEntityException if an error occurs
     */
    public static List<GenericValue> getKeyboardShortcutsForLoginPage(String applicationName, String screenName,
            Delegator delegator) throws GenericEntityException {

        List<GenericValue> shortcuts = delegator.findByAnd("KeyboardShortcutAndHandler",
                UtilMisc.toList(EntityCondition.makeCondition("userLoginId", EntityOperator.EQUALS, null),
                        EntityCondition.makeCondition(EntityOperator.OR,
                                EntityCondition.makeCondition("applicationName", EntityOperator.EQUALS,
                                        applicationName),
                                EntityCondition.makeCondition("applicationName", EntityOperator.EQUALS, null)),
                        EntityCondition.makeCondition(EntityOperator.OR,
                                EntityCondition.makeCondition("screenName", EntityOperator.EQUALS, screenName),
                                EntityCondition.makeCondition("screenName", EntityOperator.EQUALS, null))),
                UtilMisc.toList("shortcut", "userLoginId", "screenName", "applicationName")); // nulls are returned last

        // remove global shortcuts masked / overridden by more specific ones
        Iterator<GenericValue> it = shortcuts.iterator();
        String lastShortcut = null;
        while (it.hasNext()) {
            GenericValue s = it.next();
            if (lastShortcut != null && lastShortcut.equalsIgnoreCase(s.getString("shortcut"))) {
                Debug.logWarning("Ignored masked shortcut: [" + lastShortcut + "] with action ["
                        + s.getString("actionTypeId") + " -> " + s.getString("actionTarget") + "] in screen "
                        + applicationName + "/" + screenName, MODULE);
                it.remove();
            }
            lastShortcut = s.getString("shortcut");
        }

        return shortcuts;
    }

    /**
     * Assuming theMap not null; if null will throw a NullPointerException.
     * @param <K> the key type
     * @param theMap the <code>Map<K, BigDecimal></code> where to add the value
     * @param mapKey the key in the map where to add the value
     * @param addNumber the value to add in the map (can be null)
     * @return the new value for the given key in the map, after the value is added
     */
    public static <K> BigDecimal addInMapOfBigDecimal(Map<K, BigDecimal> theMap, K mapKey, BigDecimal addNumber) {
        BigDecimal currentNumber = theMap.get(mapKey);
        if (currentNumber == null) {
            currentNumber = BigDecimal.ZERO;
        }

        if (addNumber == null || addNumber.signum() == 0) {
            return currentNumber;
        }
        currentNumber = currentNumber.add(addNumber);
        theMap.put(mapKey, currentNumber);
        return currentNumber;
    }

    /**
     * Gets a <code>ByteWrapper</code> object for the given parameters.
     * @param delegator a <code>Delegator</code> value
     * @param dataResourceId a <code>String</code> value
     * @param https a <code>String</code> value
     * @param webSiteId a <code>String</code> value
     * @param locale a <code>Locale</code> value
     * @param rootDir a <code>String</code> value
     * @return the <code>ByteWrapper</code>
     * @exception IOException if an error occurs
     * @exception GeneralException if an error occurs
     * @deprecated for upgrade ofbiz to new version only, refactor the code later, ofbiz no longer uses ByteWrapper, instead use byte[] directly.
     */
    public static ByteWrapper getContentAsByteWrapper(Delegator delegator, String dataResourceId, String https,
            String webSiteId, Locale locale, String rootDir) throws IOException, GeneralException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataResourceWorker.streamDataResource(baos, delegator, dataResourceId, https, webSiteId, locale, rootDir);
        ByteWrapper byteWrapper = new ByteWrapper(baos.toByteArray());
        return byteWrapper;
    }

    /**
     * Get a <code>Locale</code> from the <code>HttpServletRequest</code>, or if not set get the default <code>Locale</code>.
     *
     * @param request a <code>HttpServletRequest</code> value
     * @return a <code>Locale</code> value
     */
    public static Locale getLocale(HttpServletRequest request) {
        return UtilHttp.getLocale(request);
    }

    /**
     * Get a <code>Locale</code> from a context <code>Map</code>, or if not set get the default <code>Locale</code>.
     *
     * @param context a context <code>Map</code> value
     * @return a <code>Locale</code> value
     */
    public static Locale getLocale(Map<String, ?> context) {
        return UtilMisc.ensureLocale(context.get("locale"));
    }

    /**
     * Get a <code>TimeZone</code> from the <code>HttpServletRequest</code>, or if not set return and set the default <code>TimeZone</code>.
     *
     * @param request a <code>HttpServletRequest</code> value
     * @return a <code>TimeZone</code> value
     */
    public static TimeZone getTimeZone(HttpServletRequest request) {
        TimeZone tz = UtilHttp.getTimeZone(request);
        if (tz == null) {
            tz = TimeZone.getDefault();
            UtilHttp.setTimeZone(request.getSession(), tz);
        }
        return tz;
    }

    /**
     * Get a <code>TimeZone</code> from a context <code>Map</code>, or if not set return the default <code>TimeZone</code>.
     *
     * @param context a context <code>Map</code> value
     * @return a <code>TimeZone</code> value
     */
    public static TimeZone getTimeZone(Map<String, ?> context) {
        TimeZone tz = (TimeZone) context.get("timeZone");
        if (tz == null) {
            tz = TimeZone.getDefault();
        }
        return tz;
    }

    /**
     * Checks if a service response is a success.
     * @param results a service response <code>Map</code>
     * @return a <code>boolean</code> value
     */
    public static boolean isSuccess(Map<String, Object> results) {
        if (results == null || results.get(ModelService.RESPONSE_MESSAGE) == null) {
            return false;
        }
        return ModelService.RESPOND_SUCCESS.equals(results.get(ModelService.RESPONSE_MESSAGE));
    }

    /**
     * Format string for javascript.
     * @param string a <code>String</code> value
     * @return a <code>String</code> value
     */
    public static String toJsString(String string) {
        if (UtilValidate.isEmail(string)) {
            return string;
        }
        return string.replaceAll("'", "&apos;");
    }

    /**
     * Gets the absolute path to the given file according to the servlet context.
     * Files are in /runtime/output/ and we get there from the servlet context path.
     * @param servletContext a <code>ServletContext</code> value
     * @param filename a <code>String</code> value
     * @return the absolute path
     */
    public static String getAbsoluteFilePath(ServletContext servletContext, final String filename) {
        String rootPath;

        // JBoss is a special case as the directory structure is a bit different than with the embedded tomcat server
        if (servletContext.getServerInfo().toLowerCase().contains("jboss")) {
            rootPath = servletContext.getRealPath("../");
        } else {
            rootPath = servletContext.getRealPath("../../../../");
        }

        String filePath = "/runtime/output/";
        return rootPath + filePath + filename;
    }

    /**
     * Gets the absolute path to the given file according to the servlet context.
     * Files are in /runtime/output/ and we get there from the servlet context path.
     * @param request a <code>HttpServletRequest</code> value
     * @param filename a <code>String</code> value
     * @return the absolute path
     */
    public static String getAbsoluteFilePath(HttpServletRequest request, final String filename) {
        ServletContext servletContext = request.getSession().getServletContext();
        return getAbsoluteFilePath(servletContext, filename);
    }

    /**
     * Split string to Vector.
     * @param ids a <code>String</code> value
     * @return a <code>Vector</code> value
     */
    public static Vector<String> stringToVector(String ids) {
        Vector<String> vector = new Vector<String>();
        List<String> idList = StringUtil.split(ids, ",");
        for (String id : idList) {
            vector.add(id);
        }
        return vector;
    }

    /**
     * Checks if the given field name is a custom field.
     * This is used in some places to identify parameters that should be treated as custom fields, so that the code
     * can adapt automatically instead of having to change the code when new fields are introduced.
     * @param fieldName a <code>String</code> value
     * @return a <code>Boolean</code> value
     */
    public static Boolean isCustomEntityField(String fieldName) {
        return fieldName.startsWith("cust_");
    }

    public static Map<String, Object> getCustomFieldsFromServiceMap(ModelEntity model,
            Map<String, Object> customFieldsMap, String parameterNameSuffix, Delegator delegator) {
        Map<String, Object> out = new HashMap<String, Object>();
        if (customFieldsMap != null) {
            for (String n : model.getAllFieldNames()) {
                String custKey = n;
                if (UtilValidate.isNotEmpty(parameterNameSuffix)) {
                    custKey = custKey + parameterNameSuffix;
                }
                custKey = custKey.substring(5); // 5 is length of "cust_"
                if (customFieldsMap.containsKey(custKey)) {
                    Object value = customFieldsMap.get(custKey);
                    out.put(n, model.convertFieldValue(n, value, delegator));
                }
            }
        }
        return out;
    }

    public static void setCustomFieldsFromServiceMap(GenericValue entity, Map<String, Object> customFieldsMap,
            String parameterNameSuffix, Delegator delegator) {
        ModelEntity model = entity.getModelEntity();
        if (customFieldsMap != null) {
            for (String n : model.getAllFieldNames()) {
                String custKey = n;
                if (UtilValidate.isNotEmpty(parameterNameSuffix)) {
                    custKey = custKey + parameterNameSuffix;
                }
                custKey = custKey.substring(5); // 5 is length of "cust_"
                if (customFieldsMap.containsKey(custKey)) {
                    Object value = customFieldsMap.get(custKey);
                    entity.set(n, model.convertFieldValue(n, value, delegator));
                }
            }
        }
    }

    public static void setCustomFieldsFromServiceMap(EntityInterface entity, Map<String, Object> customFieldsMap,
            String parameterNameSuffix, Delegator delegator) {
        ModelEntity model = delegator.getModelEntity(entity.getBaseEntityName());
        if (customFieldsMap != null) {
            for (String n : model.getAllFieldNames()) {
                String custKey = n;
                if (UtilValidate.isNotEmpty(parameterNameSuffix)) {
                    custKey = custKey + parameterNameSuffix;
                }
                custKey = custKey.substring(5); // 5 is length of "cust_"
                if (customFieldsMap.containsKey(custKey)) {
                    Object value = customFieldsMap.get(custKey);
                    entity.set(n, model.convertFieldValue(n, value, delegator));
                }
            }
        }
    }

    /**
     * Returns full email address containing a domain address and personal name for
     * the specified contact mech identifier.
     * @param contactMechId A contact mechanism identifier.
     * @param delegator An instance of the <code>Delegator</code>.
     * @return
     * @throws GenericEntityException
     */
    public static String emailAndPersonalName(String contactMechId, Delegator delegator)
            throws GenericEntityException {
        if (UtilValidate.isEmail(contactMechId)) {
            throw new IllegalArgumentException();
        }

        GenericValue contactMech = delegator.findByPrimaryKey("ContactMech",
                UtilMisc.toMap("contactMechId", contactMechId));
        if (contactMech == null) {
            Debug.logWarning(String.format("There is no ContactMech entity w/ identifier [%1$s]", contactMechId),
                    MODULE);
            return null;
        }

        String emailAddr = contactMech.getString("infoString");

        List<GenericValue> validPartyContactMechs = delegator.findByCondition("PartyContactMech",
                EntityCondition.makeCondition(UtilMisc.toList(EntityUtil.getFilterByDateExpr(),
                        EntityCondition.makeCondition("contactMechId", contactMechId))),
                UtilMisc.toList("partyId", "contactMechId"), null);

        String partyId = EntityUtil.getFirst(validPartyContactMechs).getString("partyId");

        // we will be careful and return the address only if multiple relations between
        // party and contact exist.
        if (validPartyContactMechs == null || validPartyContactMechs.size() != 1 || UtilValidate.isEmail(partyId)) {
            return emailAddr;
        }

        return emailAndPersonalName(emailAddr, partyId, delegator);
    }

    /**
     * Returns full email address containing a domain address and personal name for
     * the specified contact address and party identifier.
     * @param email An email address in format user@host 
     * @param partyId A party identifier
     * @param delegator An instance of the <code>Delegator</code>.
     * @return
     */
    public static String emailAndPersonalName(String email, String partyId, Delegator delegator) {
        return String.format("%1$s <%2$s>",
                org.ofbiz.party.party.PartyHelper.getPartyName(delegator, partyId, false), email);
    }

    /*
     * It's for fixing URL in view history so that after browser restart special characters such as & are not changed into &#63;
     * and re-arrange parameters in the url 
     */
    public static String getCorrectUrlFromEncodeUrl(String url) {
        String prevPath = url.substring(0, url.lastIndexOf("/") + 1);
        String contextUrl = url.substring(url.lastIndexOf("/") + 1);
        // if we cannot found ? in url, that meaning it was a wrong url 
        if (contextUrl.indexOf("&") >= 0 && contextUrl.indexOf("?") < 0) {
            Pattern p = Pattern.compile(SESSION_ID_PARTTER);
            Matcher m = p.matcher(contextUrl);
            String sessionId = null;
            if (m.find()) {
                sessionId = m.group();
                contextUrl = contextUrl.replace(sessionId, "");
            }
            // replace session id parttern
            contextUrl = contextUrl.replace("&#63;", "&");
            contextUrl = contextUrl.replace("&#61;", "=");
            String[] parameters = contextUrl.split(PARAMETER_SEPARATE_CHAR_PARTTER);
            contextUrl = parameters[0];
            if (sessionId != null) {
                contextUrl += sessionId;
            }
            if (parameters.length > 1) {
                contextUrl += "?" + parameters[1];
            }
            for (int i = 2; i < parameters.length; i++) {
                contextUrl += "&" + parameters[i];
            }
            return prevPath + contextUrl;
        } else {
            return url;
        }

    }
}