package er.extensions.eof; import java.util.Enumeration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOModel; import com.webobjects.eoaccess.EOModelGroup; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSComparator; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSPropertyListSerialization; import com.webobjects.foundation.NSTimestampFormatter; import er.extensions.crypting.ERXCrypto; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXProperties; /** * * @property er.extensions.ERXEOEncodingUtilities.EntityNameSeparator * @property er.extensions.ERXEOEncodingUtilities.SpecifySeparatorInURL */ public class ERXEOEncodingUtilities { private static final Logger log = LoggerFactory.getLogger(ERXEOEncodingUtilities.class); /** * Holds the default entity name separator * that is used when objects are encoded into urls. * Default value is: _ and it must not equal * <code>AttributeValueSeparator</code> */ private static String EntityNameSeparator = "_"; /** * Holds the attribute value separator used * when objects with compound keys are encoded into urls. * Its value is: . */ //immutable to avoid changing encoding/decoding methods... private static String AttributeValueSeparator = "."; /** Key used in EOModeler to specify the encoded (or abbreviated) entity * named used when encoding an enterprise-object is an url. */ public final static String EncodedEntityNameKey = "EncodedEntityName"; /** This dictionary contains the encoded entity names used in the defaultGroup */ protected static NSMutableDictionary _encodedEntityNames = null; private static boolean SpecifySeparatorInURL = true; private static boolean initialized; /** Class initialization */ public synchronized static void init() { // Find out if the user has set properties different than the defaults // EntityNameSeparator String entityNameSep = System.getProperty("er.extensions.ERXEOEncodingUtilities.EntityNameSeparator"); if ((entityNameSep != null) && (entityNameSep.length() > 0) && !entityNameSep.equals(AttributeValueSeparator)) setEntityNameSeparator(entityNameSep); // Specify separator in link ? setSpecifySeparatorInURL(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXEOEncodingUtilities.SpecifySeparatorInURL", true)); initialized = true; } public static void setSpecifySeparatorInURL(boolean specifySeparatorInURL) { SpecifySeparatorInURL = specifySeparatorInURL; } public static boolean isSpecifySeparatorInURL() { return SpecifySeparatorInURL; } public static void setEntityNameSeparator(String entityNameSeparator) { EntityNameSeparator = entityNameSeparator; } public static String entityNameSeparator() { if (!initialized) { init(); } return EntityNameSeparator; } /** * Returns enterprise objects grouped by entity name. * The specific encoding is specified in the method: <code>encodeEnterpriseObjectsPrimaryKeyForUrl</code>. * * @param ec the editing context to fetch the objects from * @param formValues dictionary where the values are an * encoded representation of the primary key values in either * cleartext or encrypted format. * @return enterprise objects grouped by entity name */ //DELETEME?: grouping objects is not this class' responsibility... public static NSDictionary groupedEnterpriseObjectsFromFormValues(EOEditingContext ec, NSDictionary formValues) { NSArray formValueObjects = decodeEnterpriseObjectsFromFormValues(ec, formValues); return ERXArrayUtilities.arrayGroupedByKeyPath(formValueObjects, "entityName"); } /** * Returns the enterprise object fetched with decoded <code>formValues</code> from * <code>entityName</code>. * @param ec the editing context to fetch the object from * @param entityName the entity to fetch the object from * @param formValues dictionary where the values are an * encoded representation of the primary key values in either * cleartext or encrypted format. * @return the enterprise object */ public static EOEnterpriseObject enterpriseObjectForEntityNamedFromFormValues(EOEditingContext ec, String entityName, NSDictionary formValues) { NSArray entityGroup = enterpriseObjectsForEntityNamedFromFormValues(ec, entityName, formValues); if (entityGroup.count() > 1) log.warn("Multiple objects for entity name: {} expecting one. objects: {}", entityName, entityGroup); return entityGroup.count() > 0 ? (EOEnterpriseObject)entityGroup.lastObject() : null; } /** * Returns the enterprise objects fetched with decoded <code>formValues</code> from * <code>entityName</code>. * @param ec the editing context to fetch the objects from * @param entityName the entity to fetch the objects from * @param formValues dictionary where the values are an * encoded representation of the primary key values in either * cleartext or encrypted format. * @return the enterprise objects */ public static NSArray enterpriseObjectsForEntityNamedFromFormValues(EOEditingContext ec, String entityName, NSDictionary formValues) { NSArray formValueObjects = decodeEnterpriseObjectsFromFormValues(ec, formValues); NSDictionary groups = ERXArrayUtilities.arrayGroupedByKeyPath(formValueObjects, "entityName"); EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName); NSMutableArray entityGroup = new NSMutableArray(); if (entity != null && entity.isAbstractEntity()) { for (Enumeration e = ERXEOAccessUtilities.allSubEntitiesForEntity(entity, false).objectEnumerator(); e.hasMoreElements();) { EOEntity subEntity = (EOEntity)e.nextElement(); NSArray aGroup = (NSArray)groups.objectForKey(subEntity.name()); if (aGroup != null) entityGroup.addObjectsFromArray(aGroup); } } else { entityGroup.addObjectsFromArray((NSArray)groups.objectForKey(entityName)); } return entityGroup; } /** * This method encodes the entity name of the enterprise object * by searching in the default model group whether it can find * the key EncodedEntityNameKey in the user info dictionary. * @param eo the enterprise object * @return the encoded entity name defaulting to the given eo's entityName */ public static String entityNameEncode (EOEnterpriseObject eo) { // Get the EncodedEntityName of the object // Default to eo's entityName String encodedEntityName = eo.entityName(); EOEntity entity = EOModelGroup.defaultGroup ().entityNamed (eo.entityName ()); NSDictionary userInfo = entity.userInfo (); if (userInfo != null && userInfo.objectForKey (EncodedEntityNameKey) != null) encodedEntityName = (String)userInfo.objectForKey (EncodedEntityNameKey); return encodedEntityName; } /** * This method constructs a dictionary with encoded * entity names as keys and entity names as values. * @return the shared dictionary containing encoded entity names. */ // FIXME: (tuscland) Should we listen to model group notifications ? // If this method is called too early, we might not have all the entities in the model group, // but this case is rare. protected static final NSDictionary encodedEntityNames () { if (_encodedEntityNames == null) { synchronized(ERXEOEncodingUtilities.class) { if(_encodedEntityNames == null) { _encodedEntityNames = new NSMutableDictionary (); NSArray models = EOModelGroup.defaultGroup().models(); for (Enumeration en = models.objectEnumerator (); en.hasMoreElements ();) { NSArray entities = ((EOModel)en.nextElement ()).entities (); for (Enumeration entEn = entities.objectEnumerator (); entEn.hasMoreElements ();) { EOEntity entity = (EOEntity)entEn.nextElement (); NSDictionary userInfo = entity.userInfo (); if(userInfo != null) { String encodedEntityName = (String)userInfo.objectForKey (EncodedEntityNameKey); if (encodedEntityName != null) _encodedEntityNames.setObjectForKey (entity.name (), encodedEntityName); } } } } } } return _encodedEntityNames; } /** * Decodes the encoded entity name. * @param encodedName the encode name. * @return decoded entity name. */ public static String entityNameDecode (String encodedName) { String entityName = encodedName; NSDictionary entityNames = encodedEntityNames (); synchronized (entityNames) { entityName = (String)entityNames.objectForKey (encodedName); } return entityName; } /** * Constructs the form values dictionary by first calling * the method <code>encodeEnterpriseObjectsPrimaryKeyForUrl</code> * and then using the results of that to construct the dictionary. * @param eos array of enterprise objects to be encoded in the url * @param separator to be used to separate entity names * @param encrypt flag to determine if the primary key * of the objects should be encrypted. * @return dictionary containing all of the key value pairs where * the keys denote the entity names and the values denote * the possibly encrypted primary keys. */ public static NSDictionary dictionaryOfFormValuesForEnterpriseObjects(NSArray eos, String separator, boolean encrypt){ String base = encodeEnterpriseObjectsPrimaryKeyForUrl(eos, separator, encrypt); NSArray elements = NSArray.componentsSeparatedByString(base, "&"); return(NSDictionary)NSPropertyListSerialization.propertyListFromString("{"+elements.componentsJoinedByString(";")+";}"); } /** * Simple cover method that calls the method: <code> * encodeEnterpriseObjectsPrimaryKeyForUrl</code> with * an array containing the single object passed in. * @param eo enterprise object to encode in a url. * @param seperator to be used for the entity name. * @param encrypt flag to determine if the primary key * of the object should be encrypted. * @return url string containing the encoded enterprise * object plus the entity name seperator used. */ public static String encodeEnterpriseObjectPrimaryKeyForUrl(EOEnterpriseObject eo, String seperator, boolean encrypt) { return encodeEnterpriseObjectsPrimaryKeyForUrl(new NSArray(eo), seperator, encrypt); } /** * Encodes an array of enterprise objects for use in a url. The * basic idea is is to have an entity name to primary key map that * makes it easy to retrieve at a later date. In addition the entity * name key will be able to tell if the value is an encrypted key or * not. In this way given a key value pair the object can be fetched * from an editing context given that at point you will know the entity * name and the primary key. * <p> * For example imagine that an array containing two User objects(pk 13 and 24) * and one Company object(pk 56) are passed to this method, null is passed in * for the separator which means the default seperator will be used which is * '_' and false is passed for encryption. Then the url that would be generated * would be: sep=_&User_1=13&User_2=24&Company_3=56 * <p> * If on the other hand let's say you use the _ character in entity names and you * want the primary keys encrypted then passing in the same array up above but with * "##" specified as the separator and true for the encrypt boolean would yield: * sep=##&User##E1=SOMEGARBAGE8723&User##E2=SOMEGARBAGE23W&Company##E3=SOMEGARBAGE8723 * <p> * Note that in the above encoding the seperator is always passed and the upper case * E specifies if the corresponding value should be decrypted. * Compound primary keys are supported, giving the following url: * sep=_&EntityName_1=1.1&EntityName_2=1.2 * where <code>1.1</code> and <code>1.2</code> are the primary key values. Key values * follow alphabetical order for their attribute names, just like * <code>ERXEOControlUtilities.primaryKeyArrayForObject</code>. * Note: At the moment the attribute value separator cannot be changed. * <h3>EncodedEntityName</h3> * You can specify an abbreviation for the encoded entityName. * This is very useful when you don't want to disclose the internals of your application * or simply because the entity name is rather long. * To do this: * <ul> * <li>open EOModeler,</li> * <li>click on the entity you want to edit,</li> * <li>get the "Info Panel"</li> * <li>go to the "User Info" tab (the last tab represented by a book)</li> * <li>add a key named <b>EncodedEntityName</b> with the value you want</li> * </ul> * * @param eos array of enterprise objects to be encoded in the url * @param separator to be used between the entity name and a sequence number * @param encrypt indicates if the primary keys of the objects should be encrypted * @return encoding of the objects passed that can be used as parameters in a url. */ // ENHANCEME: Could also place a sha hash of the blowfish key in the form values so we can know if we are using // the correct key for decryption. public static String encodeEnterpriseObjectsPrimaryKeyForUrl(NSArray eos, String separator, boolean encrypt) { // Array of objects to be encoded NSMutableArray encoded = new NSMutableArray(); // If the separator is not specified, default to the one given at class init if (separator == null) separator = entityNameSeparator(); // Add the separator if needed if (isSpecifySeparatorInURL()) encoded.addObject ("sep=" + separator); int c = 1; // Iterate through the objects for(Enumeration e = eos.objectEnumerator(); e.hasMoreElements();) { EOEnterpriseObject eo =(EOEnterpriseObject)e.nextElement(); // Get the primary key of the object NSArray pkValues = ERXEOControlUtilities.primaryKeyArrayForObject(eo); if( pkValues == null && eo instanceof ERXGeneratesPrimaryKeyInterface) { NSDictionary<String, Object> pkDict = ((ERXGeneratesPrimaryKeyInterface)eo).rawPrimaryKeyDictionary(false); if( pkDict != null ) pkValues = pkDict.allValues(); } if(pkValues == null) throw new RuntimeException("Primary key is null for object: " + eo); String pk = pkValues.componentsJoinedByString( AttributeValueSeparator ); // Get the EncodedEntityName of the object String encodedEntityName = entityNameEncode (eo); // Add the result to the list of encoded objects encoded.addObject(encodedEntityName + separator + (encrypt ? "E" : "") + c++ + "=" + (encrypt ? ERXCrypto.crypterForAlgorithm(ERXCrypto.BLOWFISH).encrypt(pk) : pk)); } // Return the result as an url-encoded string return encoded.componentsJoinedByString("&"); } /** * Decodes all of the objects for a given set of form values in * the given editing context. The object encoding is very simple, * just a generic entity name primary key pair where the key is * potentially encrypted using blowfish. The specific encoding is * specified in the method: <code>encodeEnterpriseObjectsPrimaryKeyForUrl * </code>. * @param ec editingcontext to fetch the objects from * @param values form value dictionary where the values are an * encoded representation of the primary key values in either * cleartext or encrypted format. * @return array of enterprise objects corresponding to the passed * in form values. */ public static NSArray decodeEnterpriseObjectsFromFormValues(EOEditingContext ec, NSDictionary values) { if (ec == null) throw new IllegalArgumentException("Attempting to decode enterprise objects with null editing context."); if (values == null ) throw new IllegalArgumentException("Attempting to decode enterprise objects with null form values."); log.debug("values = {}", values); NSMutableArray result = new NSMutableArray(); String separator = values.objectForKey( "sep" ) != null ? (String) values.objectForKey( "sep" ) : entityNameSeparator(); for(Enumeration e = values.keyEnumerator(); e.hasMoreElements();) { Object o = e.nextElement(); String key =(String)o; if(key.indexOf(separator) != -1) { boolean isEncrypted = key.indexOf(separator + "E") != -1; String encodedEntityName = key.substring(0, key.indexOf(separator)); String entityName = entityNameDecode (encodedEntityName); entityName = entityName == null ? encodedEntityName : entityName; EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName); if(entity != null) { NSDictionary pk = processPrimaryKeyValue( (String) values.objectForKey(key), entity, isEncrypted ); result.addObject ( EOUtilities.objectWithPrimaryKey(ec, entity.name(), pk) ); } else { log.warn("Unable to find entity for name: {}", entityName); } } } return result; } /** * Generates an NSDictionary representing primary key values, both simple and compound. If values are encrypted we try to * create the correct attribute value type. Supported types are: strings, numbers, timestamps and custom * attributes with a factory method using a string argument. * * @param value the primary key value, either a single value or a collection * @param entity the entity used to gather primary key information * @param isEncrypted yes/no * @return a dictionary with primary key values */ private static NSDictionary processPrimaryKeyValue( String value, EOEntity entity, boolean isEncrypted ) { NSArray pkAttributeNames = entity.primaryKeyAttributeNames(); try { pkAttributeNames = pkAttributeNames.sortedArrayUsingComparator ( NSComparator.AscendingStringComparator ); } catch( NSComparator.ComparisonException ex ) { log.error( "Unable to sort attribute names.", ex); throw new NSForwardException(ex); } NSArray values = isEncrypted ? NSArray.componentsSeparatedByString( ERXCrypto.crypterForAlgorithm(ERXCrypto.BLOWFISH).decrypt(value).trim(), AttributeValueSeparator ) : NSArray.componentsSeparatedByString( value, AttributeValueSeparator ); int attrCount = pkAttributeNames.count(); NSMutableDictionary result = new NSMutableDictionary( attrCount ); for( int i = 0; i < attrCount; i++ ) { String currentAttributeName = (String)pkAttributeNames.objectAtIndex( i ); EOAttribute currentAttribute = entity.attributeNamed( currentAttributeName ); Object currentValue = values.objectAtIndex( i ); switch ( currentAttribute.adaptorValueType() ) { case 3: NSTimestampFormatter tsf = new NSTimestampFormatter(); try { currentValue = tsf.parseObject( (String) currentValue ); } catch( java.text.ParseException ex ) { log.error("Error while trying to parse: {}", currentValue); throw new NSForwardException( ex ); } case 1: if( currentAttribute.valueFactoryMethodName() != null ) { currentValue = currentAttribute.newValueForString( (String) currentValue ); } case 0: currentValue = new java.math.BigDecimal( (String) currentValue ); } result.setObjectForKey( currentValue, currentAttributeName ); } return result; } }