// // ERD2WDirectAction.java: Class file for WO Component 'ERD2WDirectAction' // Project ERDirectToWeb // // Created by ak on Mon Apr 22 2002 // package er.directtoweb; import java.util.Enumeration; import org.apache.log4j.Logger; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WORequest; import com.webobjects.directtoweb.D2W; import com.webobjects.directtoweb.D2WContext; import com.webobjects.directtoweb.D2WPage; import com.webobjects.directtoweb.ERD2WContext; import com.webobjects.directtoweb.EditPageInterface; import com.webobjects.directtoweb.EditRelationshipPageInterface; import com.webobjects.directtoweb.ErrorPageInterface; import com.webobjects.directtoweb.InspectPageInterface; import com.webobjects.directtoweb.ListPageInterface; import com.webobjects.directtoweb.QueryPageInterface; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EODatabaseDataSource; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOAndQualifier; import com.webobjects.eocontrol.EOArrayDataSource; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EODataSource; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOKeyValueQualifier; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSTimestampFormatter; import er.directtoweb.interfaces.ERDErrorPageInterface; import er.directtoweb.pages.ERD2WEditableListPage; import er.directtoweb.pages.ERD2WQueryPage; import er.extensions.appserver.ERXDirectAction; import er.extensions.appserver.ERXHttpStatusCodes; import er.extensions.appserver.ERXResponse; import er.extensions.eof.ERXEC; import er.extensions.eof.ERXEOAccessUtilities; import er.extensions.eof.ERXEOControlUtilities; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXValueUtilities; /** * Automatically creates page configurations from URLs. * <h3>Examples:</h3> * <ul> * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/QueryAll</code><br > * will create an query page all entities. * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/QueryArticle</code><br > * will create an query page for articles. * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/QueryArticle?__fs=findNewArticles</code><br > * will create an query page for fetch spec "findNewArticles". This will only work if your rules return a ERD2WQueryPageWithFetchSpecification. * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/InspectArticle?__key=<articleid></code><br > * will create an inpect page for the given article. * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/EditArticle?__key=<articleid></code><br > * will create an edit page for the given article. * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/CreateArticle</code><br > * will create an edit page for a newly created article. * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/ListArticle?__key=<userid>&__keypath=User.articles</code><br > * will list the articles of the given user. * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/ListArticle?__fs=recentArticles&authorName=*foo*</code><br > * will list the articles by calling the fetch spec "recentArticles". When the * fetch spec has an "authorName" binding, it is set to "*foo*". * * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/ListArticle?__fs=&author.name=*foo*&__fs_fetchLimit=0</code><br > * will list the articles by creating a fetch spec with the supplied attributes. * When the value contains "*", then it will be regarded as a LIKE query, otherwise as a EQUAL * <li><code>http://localhost/cgi-bin/WebObjects/MyApp.woa/wa/ErrorSomeStuff?__message=Some+Test</code><br > * will create an error page with the title "Error Some Stuff" (or whatever your localizer does with it) and the message "Some Test". * </ul> * To provide some security, you should override {@link #allowPageConfiguration(String)}. Also, this * class is abstract, so you need to subclass it. */ public abstract class ERD2WDirectAction extends ERXDirectAction { /** logging support */ protected static final Logger log = Logger.getLogger(ERD2WDirectAction.class); protected final Logger actionLog = Logger.getLogger(ERD2WDirectAction.class.getName() + ".actions"); /** * Public constructor * @param r current request */ public ERD2WDirectAction(WORequest r) { super(r); } /** * primaryKeyKey is used to identity a given object via it's primary key. */ public static final String primaryKeyKey = "__key"; /** * keyPathKey is used to get relationships of a given object. */ public static final String keyPathKey = "__keypath"; /** * fetchSpecificationKey is used to get the named fetchspec of a given object. */ public static final String fetchSpecificationKey = "__fs"; /** * fetchLimit for the fetchSpec. */ public static final String fetchLimitKey = "__fs_fetchLimit"; /** * fetchLimit for the fetchSpec. */ public static final String usesDistinctKey = "__fs_usesDistinct"; /** denotes the context ID for the previous page */ public static final String contextIDKey = "__cid"; public static final String createPrefix = "Create"; /** For edit pages, we always use a fresh editing context. */ protected EOEditingContext newEditingContext() { return ERXEC.newEditingContext(session().defaultEditingContext().parentObjectStore()); } /** * Overwrite for custom value conversion. * @param attribute * @param stringValue */ protected Object qualifierValueForAttribute(EOAttribute attribute, String stringValue) { //AK: I still don't like this...in particular the new NSTimestampFormatter() which would be totally arbitrary... return ERXStringUtilities.attributeValueFromString(attribute, stringValue, context().request().formValueEncoding(), new NSTimestampFormatter()); } /** Retrieves and executes the fetch specification given in the request. */ public EOFetchSpecification fetchSpecificationFromRequest(String entityName) { EOFetchSpecification fs = null; if(context().request().formValueKeys().containsObject(fetchSpecificationKey)) { String fsName = context().request().stringFormValueForKey(fetchSpecificationKey); if(ERXStringUtilities.stringIsNullOrEmpty(fsName)) { EOEntity rootEntity = ERXEOAccessUtilities.entityNamed(session().defaultEditingContext(), entityName); NSMutableArray qualifiers = new NSMutableArray(); for(Enumeration e = context().request().formValueKeys().objectEnumerator(); e.hasMoreElements(); ) { String key = (String)e.nextElement(); EOEntity entity = rootEntity; EOAttribute attribute = null; String attributeName = key; if(key.indexOf(".") > 0) { String path = ERXStringUtilities.keyPathWithoutLastProperty(key); attributeName = ERXStringUtilities.lastPropertyKeyInKeyPath(key); entity = ERXEOAccessUtilities.destinationEntityForKeyPath(rootEntity, path); } if(entity != null) { attribute = entity.attributeNamed(attributeName); if(attribute != null) { String stringValue = context().request().stringFormValueForKey(key); if(stringValue != null) { Object value = null; NSSelector selector = EOKeyValueQualifier.QualifierOperatorEqual; if(stringValue.indexOf('*') >= 0) { selector = EOKeyValueQualifier.QualifierOperatorCaseInsensitiveLike; } if(!NSKeyValueCoding.NullValue.toString().equals(stringValue)) { //AK: I still don't like this...in particular the new NSTimestampFormatter() which would be totally arbitrary... value = qualifierValueForAttribute(attribute, stringValue); if(value!=null) { qualifiers.addObject(new EOKeyValueQualifier(key, selector, value)); } } else { qualifiers.addObject(new EOKeyValueQualifier(key, selector, value)); } } } } } EOQualifier qualifier = null; if(qualifiers.count() > 0) { qualifier = new EOAndQualifier(qualifiers); } fs = new EOFetchSpecification(entityName, qualifier, null); boolean usesDictinct = ERXValueUtilities.booleanValueWithDefault(context().request().stringFormValueForKey(usesDistinctKey), true); fs.setUsesDistinct(usesDictinct); int limit = ERXValueUtilities.intValueWithDefault(context().request().stringFormValueForKey(fetchLimitKey), 200); fs.setFetchLimit(limit); } else { fs = EOFetchSpecification.fetchSpecificationNamed(fsName, entityName); NSMutableDictionary bindings = new NSMutableDictionary(); Enumeration e = fs.qualifier().bindingKeys().objectEnumerator(); while(e.hasMoreElements()) { String key = (String)e.nextElement(); String formValue = context().request().stringFormValueForKey(key); if(formValue != null) { bindings.setObjectForKey(formValue, key); } } if(bindings.count() > 0) { fs = fs.fetchSpecificationWithQualifierBindings(bindings); } } } return fs; } public NSDictionary primaryKeyFromRequest(EOEditingContext ec, String entityName) { String pkString = context().request().stringFormValueForKey(primaryKeyKey); return ERXEOControlUtilities.primaryKeyDictionaryForString(ec, entityName, pkString); } public WOComponent previousPageFromRequest() { String cid = context().request().stringFormValueForKey(contextIDKey); if (cid == null) return context().page(); return session().restorePageForContextID(cid); } public String keyPathFromRequest() { return context().request().stringFormValueForKey(keyPathKey); } public EOArrayDataSource relationshipArrayFromRequest(EOEditingContext ec, EOClassDescription cd) { String keyPath = context().request().stringFormValueForKey(keyPathKey); if(keyPath != null) { int indexOfDot = keyPath.indexOf("."); if(indexOfDot > 0) { String entityName = keyPath.substring(0, indexOfDot); String relationshipPath = keyPath.substring(indexOfDot+1, keyPath.length()); EOEnterpriseObject eo = EOUtilities.objectWithPrimaryKey(ec, entityName, primaryKeyFromRequest(ec, entityName)); EOArrayDataSource ds = new EOArrayDataSource(cd, ec); ds.setArray((NSArray)eo.valueForKeyPath(relationshipPath)); return ds; } } return null; } protected void prepareEditPage(D2WContext context, EditPageInterface epi, String entityName) { EOEditingContext ec = newEditingContext(); EOEnterpriseObject eo = null; ec.lock(); try { if(context.dynamicPage().startsWith(createPrefix) || primaryKeyFromRequest(ec, entityName) == null) { eo = EOUtilities.createAndInsertInstance(ec,entityName); } else { eo = ERXEOControlUtilities.objectWithPrimaryKeyValue(ec, entityName, primaryKeyFromRequest(ec, entityName), null); } } finally { ec.unlock(); } epi.setObject(eo); epi.setNextPage(previousPageFromRequest()); } protected void prepareInspectPage(D2WContext context, InspectPageInterface ipi, String entityName) { EOEditingContext ec = session().defaultEditingContext(); EOEnterpriseObject eo = null; ec.lock(); try { eo = EOUtilities.objectWithPrimaryKey(ec, entityName, primaryKeyFromRequest(ec, entityName)); } finally { ec.unlock(); } ipi.setObject(eo); ipi.setNextPage(previousPageFromRequest()); } protected void prepareQueryPage(D2WContext context, QueryPageInterface qpi, String entityName) { EOFetchSpecification fs = fetchSpecificationFromRequest(entityName); if(qpi instanceof ERD2WQueryPage) { if(fs != null) ((ERD2WQueryPage)qpi).setFetchSpecification(fs); } } protected void prepareEditRelationshipPage(D2WContext context, EditRelationshipPageInterface erpi, String entityName) { EOEditingContext ec = ERXEC.newEditingContext(session().defaultEditingContext().parentObjectStore()); String keypath = keyPathFromRequest(); String masterEntityName = ERXStringUtilities.firstPropertyKeyInKeyPath(keypath); String relationshipKey = ERXStringUtilities.keyPathWithoutFirstProperty(keypath); NSDictionary pk = primaryKeyFromRequest(ec, masterEntityName); EOEnterpriseObject masterObject = ERXEOControlUtilities.objectWithPrimaryKeyValue(ec, masterEntityName, pk, null); erpi.setMasterObjectAndRelationshipKey(masterObject, relationshipKey); erpi.setNextPage(previousPageFromRequest()); } protected void prepareListPage(D2WContext context, ListPageInterface lpi, String entityName) { EOEditingContext ec = session().defaultEditingContext(); //ak: this check could be better...but anyway, as we edit, we should get a peer context if(lpi instanceof ERD2WEditableListPage) { ec = ERXEC.newEditingContext(session().defaultEditingContext().parentObjectStore()); } EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName); EODataSource ds = relationshipArrayFromRequest(ec, entity.classDescriptionForInstances()); if(ds == null) { ds = new EODatabaseDataSource(ec, entityName); EOFetchSpecification fs = fetchSpecificationFromRequest(entityName); if(fs == null) { fs = new EOFetchSpecification(entityName, null, null); } if(!context().request().formValueKeys().contains(fetchLimitKey)) { int fetchLimit = ERXValueUtilities.intValueWithDefault(context.valueForKey("fetchLimit"), 200); fs.setFetchLimit(fetchLimit); } boolean refresh = ERXValueUtilities.booleanValueWithDefault(context.valueForKey("refreshRefetchedObjects"), false); fs.setRefreshesRefetchedObjects(refresh); ((EODatabaseDataSource)ds).setFetchSpecification(fs); } lpi.setDataSource(ds); lpi.setNextPage(previousPageFromRequest()); } public WOActionResults dynamicPageForActionNamed(String anActionName) { WOComponent newPage = null; try { newPage = D2W.factory().pageForConfigurationNamed(anActionName, session()); } catch (IllegalStateException ex) { // this will get thrown when a page simply isn't found. We don't really need to report it actionLog.debug("dynamicPageForActionNamed failed for Action:" + anActionName, ex); return null; } D2WContext context = null; if(newPage instanceof D2WPage) { context = ((D2WPage)newPage).d2wContext(); } else { context = ERD2WContext.newContext(session()); context.setDynamicPage(anActionName); } EOEntity entity = context.entity(); if(entity != null) { String entityName = entity.name(); String taskName = context.task(); if(newPage instanceof EditPageInterface && taskName.equals("edit")) { prepareEditPage(context, (EditPageInterface)newPage, entityName); } else if(newPage instanceof InspectPageInterface) { prepareInspectPage(context, (InspectPageInterface)newPage, entityName); } else if(newPage instanceof QueryPageInterface) { prepareQueryPage(context, (QueryPageInterface)newPage, entityName); } else if(newPage instanceof EditRelationshipPageInterface) { prepareEditRelationshipPage(context, (EditRelationshipPageInterface)newPage, entityName); } else if(newPage instanceof ListPageInterface) { prepareListPage(context, (ListPageInterface)newPage, entityName); } else if(newPage instanceof ErrorPageInterface) { prepareErrorPage(context, (ErrorPageInterface)newPage); } } else if(newPage instanceof ErrorPageInterface) { prepareErrorPage(context, (ErrorPageInterface)newPage); } return newPage; } /** * Returns an error page and sets the message to the key<code> __message</code>. */ protected WOActionResults prepareErrorPage(D2WContext d2wContext, ErrorPageInterface epi) { WOActionResults newPage = null; try { String message = context().request().stringFormValueForKey("__message"); // AK: actually, this isn't enough to prevent hacks, as you might also be able // to social-engineer your way around. We should simply use a key into the localizer. if(message != null) { message = message.replaceAll("<.*?>", ""); } epi.setMessage(message); epi.setNextPage(previousPageFromRequest()); newPage = (WOActionResults)epi; } catch (Exception otherException) { log.error("Exception while trying to report exception!", otherException); } return newPage; } /** * Creates an error page with the given exception. * @param ex */ public WOActionResults reportException(Exception ex) { WOActionResults newPage = null; try { ErrorPageInterface epf=D2W.factory().errorPage(session()); if(epf instanceof ERDErrorPageInterface) { ((ERDErrorPageInterface)epf).setException(ex); } epf.setMessage(ex.toString()); epf.setNextPage(previousPageFromRequest()); newPage = (WOActionResults)epf; } catch (Exception otherException) { log.error("Exception while trying to report exception!", otherException); } return newPage; } /** * Checks if a page configuration is allowed to render. * Override for a more intelligent access scheme as the default just returns true. * @param pageConfiguration */ protected boolean allowPageConfiguration(String pageConfiguration) { return true; } /** * Returns a response with a 401 (access denied) message. Override this for something more user friendly. */ public WOActionResults forbiddenAction() { return new ERXResponse("Access denied", ERXHttpStatusCodes.UNAUTHORIZED); } /** * Overrides the default implementation to try to look up the action as a * page configuration if there is no method with the wanted name. This * implementation catches NoSuchMethodException more or less silently, so be * sure to turn on logging. */ @Override public WOActionResults performActionNamed(String anActionName) { WOActionResults newPage = null; try { try { if(false) throw new NoSuchMethodException(); //keep the compiler happy newPage = super.performActionNamed(anActionName); } catch (NSForwardException fwe) { if(!(fwe.originalException() instanceof NoSuchMethodException)) throw fwe; actionLog.debug("performActionNamed for action: " + anActionName, fwe); } catch (NoSuchMethodException nsm) { // this will get thrown when an action isn't found. We don't really need to report it. actionLog.debug("performActionNamed for action: " + anActionName, nsm); } if(newPage == null) { if(allowPageConfiguration(anActionName)) { newPage = dynamicPageForActionNamed(anActionName); } else { newPage = forbiddenAction(); } } } catch(Exception ex) { log.error("Error with action " + anActionName + ":" + ex + ", formValues:" + context().request().formValues(), ex); newPage = reportException(ex); } return newPage; } }