Author: gvanmatre Date: Fri May 12 11:52:01 2006 New Revision: 405832 URL: http://svn.apache.org/viewcvs?rev=405832&view=rev Log: Fix for bug SHALE-176 reported by Lucy Luo and Veit Guna. Added summary and detail validation message overrides at the component level and resource bundle registered in the faces configuration file.
Modified: struts/shale/trunk/core-library/src/conf/taglib.tld struts/shale/trunk/core-library/src/java/org/apache/shale/component/Token.java struts/shale/trunk/core-library/src/java/org/apache/shale/resources/Bundle.properties struts/shale/trunk/core-library/src/java/org/apache/shale/taglib/TokenTag.java struts/shale/trunk/core-library/src/test/org/apache/shale/util/TestBundle_en_US.properties struts/shale/trunk/core-library/src/test/org/apache/shale/util/TokenProcessorTestCase.java Modified: struts/shale/trunk/core-library/src/conf/taglib.tld URL: http://svn.apache.org/viewcvs/struts/shale/trunk/core-library/src/conf/taglib.tld?rev=405832&r1=405831&r2=405832&view=diff ============================================================================== --- struts/shale/trunk/core-library/src/conf/taglib.tld (original) +++ struts/shale/trunk/core-library/src/conf/taglib.tld Fri May 12 11:52:01 2006 @@ -140,6 +140,26 @@ </description> </attribute> + + <attribute> + <name>messageSummary</name> + <required>false</required> + <rtexprvalue>false</rtexprvalue> + <description> + Default messageSummary override used to reporting a token verification failure. + </description> + </attribute> + + + <attribute> + <name>messageDetail</name> + <required>false</required> + <rtexprvalue>false</rtexprvalue> + <description> + Default messageDetail override used to reporting a token verification failure. + </description> + </attribute> + </tag> <tag> Modified: struts/shale/trunk/core-library/src/java/org/apache/shale/component/Token.java URL: http://svn.apache.org/viewcvs/struts/shale/trunk/core-library/src/java/org/apache/shale/component/Token.java?rev=405832&r1=405831&r2=405832&view=diff ============================================================================== --- struts/shale/trunk/core-library/src/java/org/apache/shale/component/Token.java (original) +++ struts/shale/trunk/core-library/src/java/org/apache/shale/component/Token.java Fri May 12 11:52:01 2006 @@ -19,10 +19,12 @@ import javax.faces.application.FacesMessage; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; +import javax.faces.el.ValueBinding; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.shale.faces.ShaleConstants; +import org.apache.shale.util.LoadBundle; import org.apache.shale.util.Messages; import org.apache.shale.util.TokenProcessor; @@ -45,6 +47,17 @@ /** + * <p>The resource bundle <code>messageSummary</code> key.</p> + */ + private static final String MESSAGE_SUMMARY_KEY = "token.summary.invalid"; + + /** + * <p>The resource bundle <code>messageDetail</code> key.</p> + */ + private static final String MESSAGE_DETAIL_KEY = "token.detail.invalid"; + + + /** * <p>Message resources for this class */ private static Messages messages = @@ -54,7 +67,64 @@ // ------------------------------------------------------------ Constructors + /** + * <p>A validation message summary override that can be used to change the default + * validation message summary when the token verification fails.</p> + */ + private String messageSummary = null; + + /** + * <p>Returns the validation <code>messageSummary</code> used to create a + * <code>FacesMessage.SEVERITY_ERROR</code>.</p> + */ + public String getMessageSummary() { + if (null != messageSummary) + return messageSummary; + ValueBinding valuebinding = getValueBinding("messageSummary"); + if (valuebinding != null) + return (String) valuebinding.getValue(getFacesContext()); + else + return null; + } + + /** + * <p>Sets a <code>messageSummary</code> override used when reporting + * a token verification failure.</p> + */ + public void setMessageSummary(String message) { + this.messageSummary = message; + } + + /** + * <p>A validation message detail override that can be used to change the default + * validation message detail when the token verification fails.</p> + */ + private String messageDetail = null; + + /** + * <p>Returns the validation <code>messageDetail</code> used to create a + * <code>FacesMessage.SEVERITY_ERROR</code>.</p> + */ + public String getMessageDetail() { + if (null != messageDetail) + return messageDetail; + ValueBinding valuebinding = getValueBinding("messageDetail"); + if (valuebinding != null) + return (String) valuebinding.getValue(getFacesContext()); + else + return null; + } + + /** + * <p>Sets a <code>messageDetail</code> override used when reporting + * a token verification failure.</p> + */ + public void setMessageDetail(String message) { + this.messageDetail = message; + } + + /** * <p>Create a default instance of this component.</p> */ @@ -96,13 +166,91 @@ log.debug(" Validation failed!"); } setValid(false); - String summary = messages.getMessage("token.invalid"); - FacesMessage message = new FacesMessage(summary); + String summary = getErrorSummaryMessage(context); + String detail = getErrorDetailMessage(context); + FacesMessage message = new FacesMessage(summary, detail); message.setSeverity(FacesMessage.SEVERITY_ERROR); context.addMessage(getClientId(context), message); } } + + /** + * <p>Returns the validation summary message. + * The validation <code>messageSummary</code> is evaluated in the following order: + * <ol> + * <li><p>The <code>messageSummary</code> property on the [EMAIL PROTECTED] Token} component is the + * first order of override.</p></li> + * <li><p>The next level of override is a custom resource bundled registered + * in the faces-config.xml using the message key of <strong>token.summary.invalid</strong>. + * <br/><br/> + * <p><blockquote><pre> + * <application> + * <messageSummary-bundle>org.acme.resources.Bundle</messageSummary-bundle> + * </application> + *</pre></blockquote></p></li> + *<li>The default will be taken from <strong>org.apache.shale.resources.Bundle</strong> + *packaged in the core shale java archive. The default message summary is + *"<strong>Invalid resubmit of the same form</strong>".</p></li> + *</ol></p> + * + * @param context faces context + * @return invalid token message + */ + private String getErrorSummaryMessage(FacesContext context) { + String msg = null; + if ((msg = getMessageSummary()) == null) { + String bundleName = null; + if ((bundleName = context.getApplication().getMessageBundle()) != null) { + LoadBundle loadBundle = new LoadBundle(bundleName); + msg = (String) loadBundle.getMap().get(MESSAGE_SUMMARY_KEY); + } + } + + if (msg == null) + msg = messages.getMessage(MESSAGE_SUMMARY_KEY); + + return msg; + } + + + /** + * <p>Returns the validation detail message. + * The validation <code>messageDetail</code> is evaluated in the following order: + * <ol> + * <li><p>The <code>messageDetail</code> property on the [EMAIL PROTECTED] Token} component is the + * first order of override.</p></li> + * <li><p>The next level of override is a custom resource bundled registered + * in the faces-config.xml using the message key of <strong>token.detail.invalid</strong>. + * <br/><br/> + * <p><blockquote><pre> + * <application> + * <messageSummary-bundle>org.acme.resources.Bundle</messageSummary-bundle> + * </application> + *</pre></blockquote></p></li> + *<li>The default will be taken from <strong>org.apache.shale.resources.Bundle</strong> + *packaged in the core shale java archive. The default message detail is + *an empty string, "".</p></li> + *</ol></p> + * + * @param context faces context + * @return invalid token message + */ + private String getErrorDetailMessage(FacesContext context) { + String msg = null; + if ((msg = getMessageDetail()) == null) { + String bundleName = null; + if ((bundleName = context.getApplication().getMessageBundle()) != null) { + LoadBundle loadBundle = new LoadBundle(bundleName); + msg = (String) loadBundle.getMap().get(MESSAGE_DETAIL_KEY); + } + } + + if (msg == null) + msg = messages.getMessage(MESSAGE_DETAIL_KEY); + + return msg; + } // ---------------------------------------------------------- Public Methods @@ -147,6 +295,29 @@ return tp; } + + /** + * <p>Restores the components state.</p> + */ + public void restoreState(FacesContext context, Object obj) { + Object[] aobj = (Object[]) obj; + super.restoreState(context, aobj[0]); + + messageSummary = ((String) aobj[1]); + messageDetail = ((String) aobj[2]); + } + + /** + * <p>Saves the components state.</p> + */ + public Object saveState(FacesContext context) { + Object[] aobj = new Object[3]; + aobj[0] = super.saveState(context); + aobj[1] = messageSummary; + aobj[2] = messageDetail; + + return aobj; + } } Modified: struts/shale/trunk/core-library/src/java/org/apache/shale/resources/Bundle.properties URL: http://svn.apache.org/viewcvs/struts/shale/trunk/core-library/src/java/org/apache/shale/resources/Bundle.properties?rev=405832&r1=405831&r2=405832&view=diff ============================================================================== --- struts/shale/trunk/core-library/src/java/org/apache/shale/resources/Bundle.properties (original) +++ struts/shale/trunk/core-library/src/java/org/apache/shale/resources/Bundle.properties Fri May 12 11:52:01 2006 @@ -44,7 +44,8 @@ subview.noType=Managed bean for subview {0} is not a ViewController # org.apache.shale.component.Token -token.invalid=Invalid resubmit of the same form +token.summary.invalid=Invalid resubmit of the same form +token.detail.invalid= # org.apache.shale.dialog.faces.DialogNavigationHandler dialog.noDialog=You have requested a dialog named "{0}" but no such dialog definition can be found. Double check the spelling of the dialog name. Modified: struts/shale/trunk/core-library/src/java/org/apache/shale/taglib/TokenTag.java URL: http://svn.apache.org/viewcvs/struts/shale/trunk/core-library/src/java/org/apache/shale/taglib/TokenTag.java?rev=405832&r1=405831&r2=405832&view=diff ============================================================================== --- struts/shale/trunk/core-library/src/java/org/apache/shale/taglib/TokenTag.java (original) +++ struts/shale/trunk/core-library/src/java/org/apache/shale/taglib/TokenTag.java Fri May 12 11:52:01 2006 @@ -16,6 +16,9 @@ package org.apache.shale.taglib; +import javax.faces.component.UIComponent; +import javax.faces.context.FacesContext; +import javax.faces.el.ValueBinding; import javax.faces.webapp.UIComponentTag; import org.apache.shale.component.Token; @@ -29,6 +32,35 @@ /** + * <p>Pushs the tag attributes to the [EMAIL PROTECTED] Token}'s + * component properties.</p> + */ + protected void setProperties(UIComponent component) { + super.setProperties(component); + + FacesContext context = getFacesContext(); + + if (messageSummary != null) { + if (this.isValueReference(messageSummary)) { + ValueBinding vb = context.getApplication().createValueBinding(messageSummary); + component.setValueBinding("messageSummary", vb); + } else + component.getAttributes().put("messageSummary", messageSummary); + } + + if (messageDetail != null) { + if (this.isValueReference(messageDetail)) { + ValueBinding vb = context.getApplication().createValueBinding(messageDetail); + component.setValueBinding("messageDetail", vb); + } else + component.getAttributes().put("messageDetail", messageDetail); + } + + + } + + + /** * <p>Return the required component type.</p> */ public String getComponentType() { @@ -43,5 +75,50 @@ return "org.apache.shale.Token"; } + /** + * <p>A validation messageSummary override that can be used to change the default + * validation messageSummary when the token verification fails.</p> + */ + private String messageSummary = null; + + /** + * <p>Sets a <code>messageSummary</code> override used when reporting + * a [EMAIL PROTECTED] org.apache.shale.component.Token} verification failure.</p> + */ + public void setMessageSummary(String message) { + this.messageSummary = message; + } + + /** + * <p>Returns a <code>messageSummary</code> override used when reporting + * a [EMAIL PROTECTED] org.apache.shale.component.Token} verification failure.</p> + */ + public String getMessageSummary() { + return messageSummary; + } + + + /** + * <p>A validation messageDetail override that can be used to change the default + * validation messageDetail when the token verification fails.</p> + */ + private String messageDetail = null; + + /** + * <p>Sets a <code>messageDetail</code> override used when reporting + * a [EMAIL PROTECTED] org.apache.shale.component.Token} verification failure.</p> + */ + public void setMessageDetail(String message) { + this.messageDetail = message; + } + /** + * <p>Returns a <code>messageDetail</code> override used when reporting + * a [EMAIL PROTECTED] org.apache.shale.component.Token} verification failure.</p> + */ + public String getMessageDetail() { + return messageDetail; + } + + } Modified: struts/shale/trunk/core-library/src/test/org/apache/shale/util/TestBundle_en_US.properties URL: http://svn.apache.org/viewcvs/struts/shale/trunk/core-library/src/test/org/apache/shale/util/TestBundle_en_US.properties?rev=405832&r1=405831&r2=405832&view=diff ============================================================================== --- struts/shale/trunk/core-library/src/test/org/apache/shale/util/TestBundle_en_US.properties (original) +++ struts/shale/trunk/core-library/src/test/org/apache/shale/util/TestBundle_en_US.properties Fri May 12 11:52:01 2006 @@ -2,3 +2,6 @@ key1=English Key 1 key2=English Key 2 +token.summary.invalid=Invalid resubmit of the same form is bad news Lucy Luo +token.detail.invalid=This page has been mark as having durability of a single submit and this contract has been violated by the submission of a dirty page. + Modified: struts/shale/trunk/core-library/src/test/org/apache/shale/util/TokenProcessorTestCase.java URL: http://svn.apache.org/viewcvs/struts/shale/trunk/core-library/src/test/org/apache/shale/util/TokenProcessorTestCase.java?rev=405832&r1=405831&r2=405832&view=diff ============================================================================== --- struts/shale/trunk/core-library/src/test/org/apache/shale/util/TokenProcessorTestCase.java (original) +++ struts/shale/trunk/core-library/src/test/org/apache/shale/util/TokenProcessorTestCase.java Fri May 12 11:52:01 2006 @@ -16,13 +16,24 @@ package org.apache.shale.util; +import java.io.StringWriter; import java.util.HashSet; import java.util.Iterator; +import java.util.Locale; +import java.util.Map; import java.util.Set; +import javax.faces.application.FacesMessage; +import javax.faces.component.UIComponent; +import javax.faces.component.UIInput; +import javax.faces.component.UIViewRoot; +import javax.faces.context.ResponseWriter; + import junit.framework.Test; import junit.framework.TestSuite; +import org.apache.shale.component.Token; +import org.apache.shale.renderer.TokenRenderer; import org.apache.shale.test.base.AbstractJsfTestCase; /** @@ -50,7 +61,12 @@ // Set up the instance we will be testing tp = new TokenProcessor(); - + + //register the Token component + application.addComponent("org.apache.shale.Token", "org.apache.shale.component.Token"); + facesContext.getRenderKit().addRenderer("org.apache.shale.Token", + "org.apache.shale.Token", + new TokenRenderer()); } @@ -115,5 +131,243 @@ } + public void testMessagePropertyOverride() throws Exception { + + final String SUMMARY_MESSAGE = "This page is dirty. Evil browser back button."; + final String DETAIL_MESSAGE = "This page has been mark as having durability of a single submit and this contract has been violated by the submission of a dirty page."; + + Token token = (Token) facesContext.getApplication().createComponent("org.apache.shale.Token"); + assertNotNull(token); + + UIViewRoot root = facesContext.getViewRoot(); + assertNotNull(root); + + // add token to the component tree + root.getChildren().add(root.getChildCount(), token); + + token.setId("messagePropertyOverride"); + token.setMessageSummary(SUMMARY_MESSAGE); + token.setMessageDetail(DETAIL_MESSAGE); + + StringBuffer htmlSnippet = encode(token); + // check rendered markup + String id = getAttribute(htmlSnippet, "id"); + assertEquals("id", token.getClientId(facesContext), id); + + String name = getAttribute(htmlSnippet, "name"); + assertEquals("id", token.getClientId(facesContext), name); + + String type = getAttribute(htmlSnippet, "type"); + assertEquals("id", "hidden", type); + + String value = getAttribute(htmlSnippet, "value"); + assertNotNull("value", value); + + // simulate form post on dirty page + Map map = facesContext.getExternalContext().getRequestParameterMap(); + map.put(id, value); + + // simulate apply values + token.decode(facesContext); + assertEquals("value", value, token.getSubmittedValue()); + + // simulate validation and invalidates token + token.validate(facesContext); + + checkNoMessages(token); + + // simulate double post + token.validate(facesContext); + + // check for the custom message + checkMessage(SUMMARY_MESSAGE, DETAIL_MESSAGE, token); + + } + + public void testMessageFacesConfigBundleOverride() throws Exception { + + // simulate a faces config resource bundle override + application.setDefaultLocale(new Locale("en", "US")); + application.setMessageBundle("org.apache.shale.util.TestBundle"); + + //value of the "token.invalid" key in the "org.apache.shale.util.TestBundle" + final String SUMMARY_MESSAGE = "Invalid resubmit of the same form is bad news Lucy Luo"; + final String DETAIL_MESSAGE = "This page has been mark as having durability of a single submit and this contract has been violated by the submission of a dirty page."; + + Token token = (Token) facesContext.getApplication().createComponent("org.apache.shale.Token"); + assertNotNull(token); + + UIViewRoot root = facesContext.getViewRoot(); + assertNotNull(root); + + // add token to the component tree + root.getChildren().add(root.getChildCount(), token); + + token.setId("messageFacesConfigBundleOverride"); + token.setMessageSummary(null); // no message property override + token.setMessageDetail(null); // no message property override + + + StringBuffer htmlSnippet = encode(token); + // check rendered markup + String id = getAttribute(htmlSnippet, "id"); + assertEquals("id", token.getClientId(facesContext), id); + + String name = getAttribute(htmlSnippet, "name"); + assertEquals("id", token.getClientId(facesContext), name); + + String type = getAttribute(htmlSnippet, "type"); + assertEquals("id", "hidden", type); + + String value = getAttribute(htmlSnippet, "value"); + assertNotNull("value", value); + + // simulate form post on dirty page + Map map = facesContext.getExternalContext().getRequestParameterMap(); + map.put(id, value); + + // simulate apply values + token.decode(facesContext); + assertEquals("value", value, token.getSubmittedValue()); + + // simulate validation and invalidates token + token.validate(facesContext); + + checkNoMessages(token); + + // simulate double post + token.validate(facesContext); + + // check for the custom message + checkMessage(SUMMARY_MESSAGE, DETAIL_MESSAGE, token); + + } + + public void testMessageDefault() throws Exception { + + //value of the "token.invalid" key in the "org.apache.shale.resources.Bundle" + final String SUMMARY_MESSAGE = "Invalid resubmit of the same form"; + final String DETAIL_MESSAGE = ""; // none by detault + + Token token = (Token) facesContext.getApplication().createComponent("org.apache.shale.Token"); + assertNotNull(token); + + UIViewRoot root = facesContext.getViewRoot(); + assertNotNull(root); + + // add token to the component tree + root.getChildren().add(root.getChildCount(), token); + + token.setId("messageDefault"); + token.setMessageSummary(null); // no message property override + + StringBuffer htmlSnippet = encode(token); + // check rendered markup + String id = getAttribute(htmlSnippet, "id"); + assertEquals("id", token.getClientId(facesContext), id); + + String name = getAttribute(htmlSnippet, "name"); + assertEquals("id", token.getClientId(facesContext), name); + + String type = getAttribute(htmlSnippet, "type"); + assertEquals("id", "hidden", type); + String value = getAttribute(htmlSnippet, "value"); + assertNotNull("value", value); + + // simulate form post on dirty page + Map map = facesContext.getExternalContext().getRequestParameterMap(); + map.put(id, value); + + // simulate apply values + token.decode(facesContext); + assertEquals("value", value, token.getSubmittedValue()); + + // simulate validation and invalidates token + token.validate(facesContext); + + checkNoMessages(token); + + // simulate double post + token.validate(facesContext); + + // check for the custom message + checkMessage(SUMMARY_MESSAGE, DETAIL_MESSAGE, token); + + } + + + + //looks for an html attribute in a markup snippet + private String getAttribute(StringBuffer htmlSnippet, String attributeName) { + String sarg = attributeName + "=\""; + int s = htmlSnippet.indexOf(sarg); + String value = null; + if (s > -1) { + s += sarg.length(); + int e = htmlSnippet.indexOf("\"", s); + if (e > -1 && e > s) { + value = htmlSnippet.substring(s, e); + } + + } + return value; + } + + //looks for an error message added in validation + private void checkMessage(String expectedSummary, String expectedDetail, UIInput component) { + String id = component.getClientId(facesContext); + Iterator mi = facesContext.getMessages(id); + boolean hasMessage = false; + while (mi.hasNext()) { + FacesMessage message = (FacesMessage) mi.next(); + String summary = message.getSummary(); + String detail = message.getDetail(); + assertEquals(expectedSummary, summary); + assertEquals(expectedDetail, detail); + hasMessage = true; + } + + assertTrue(id + " has messages", hasMessage); + } + + //checks to make sure there are no error messages after validation + private void checkNoMessages(UIInput component) { + String id = component.getClientId(facesContext); + Iterator mi = facesContext.getMessages(id); + boolean hasMessage = false; + while (mi.hasNext()) { + hasMessage = true; + } + + assertFalse(id + " has messages", hasMessage); + + } + + + //render and return the results + private StringBuffer encode(UIComponent component) throws Exception { + //builds a buffer to write the page to + StringWriter writer = new StringWriter(); + //create a buffered response writer + ResponseWriter buffResponsewriter = facesContext.getRenderKit() + .createResponseWriter(writer, null, response.getCharacterEncoding()); + //push buffered writer to the faces context + facesContext.setResponseWriter(buffResponsewriter); + //start a document + buffResponsewriter.startDocument(); + + //render HTML + component.encodeBegin(facesContext); + component.encodeChildren(facesContext); + component.encodeEnd(facesContext); + + //end the document + buffResponsewriter.endDocument(); + + return writer.getBuffer(); + } + + + }