001    package hirondelle.web4j.ui.tag;
002    
003    import java.util.regex.*;
004    import java.util.logging.*;
005    import hirondelle.web4j.Controller;
006    import hirondelle.web4j.util.EscapeChars;
007    import hirondelle.web4j.util.Util;
008    import hirondelle.web4j.util.Regex;
009    
010    /**
011     Suppress self-linking anchor tags (links), where a 
012     link has the current page as its destination.
013    
014     <P>Self-linking is usually considered to be poor style, since the user is simply 
015     led back to the current page. 
016     
017     <P>Rendering links to the current page in a distinct manner has two benefits :
018    <ul>
019     <li>the user is prevented from following a useless link
020     <li>the user can see 'where they are' in a menu of items
021    </ul>
022     
023     <P>This tag provides two distinct techniques for identifying self-linking: 
024    <ul>
025     <li>the current URI <em>ends with</em> the link's <tt>href</tt> target. 
026     This is the default mechanism. The 'ends with' is used since the current URI 
027     (stored by the <tt>Controller</tt> in request scope) is absolute, while most 
028     <tt>HREF</tt> attributes contain relative links.    
029     <li>the trimmed link text (the body of the anchor tag) matches the <tt>TTitle</tt> request parameter 
030     used by the WEB4J templating mechanism.
031    </ul>
032     
033     <P>If these policies are inadequate, then {@link #isSelfLinkingByHref(String, String)} and 
034     {@link #isSelfLinkingByTitle(String, String)} may be overridden as desired. 
035     
036     <P>Example use case.
037    <PRE>
038    &lt;w:highlightCurrentPage styleClass="highlight"&gt;
039      &lt;a href='ShowHomePage.do'&gt;Home&lt;/a&gt;
040      &lt;a href='ShowSearch.do'&gt;Search&lt;/a&gt;
041      &lt;a href='ShowContact.do'&gt;Contact&lt;/a&gt;
042    &lt;/w:highlightCurrentPage&gt;
043    </PRE>
044     If the current URI is
045     <PRE>
046     http://www.blah.com/fish/main/home/ShowHomePage.do
047     </PRE>
048     then the output of this tag will be
049    <PRE>
050      &lt;span class="highlight"&gt;Home&lt;/span&gt;
051      &lt;a href='ShowSearch.do'&gt;Search&lt;/a&gt;
052      &lt;a href='ShowContact.do'&gt;Contact&lt;/a&gt;
053    </PRE>
054     If the <tt>styleClass</tt> attribute is not used, then the output would simply be :
055    <PRE>
056      Home
057      &lt;a href='ShowSearch.do'&gt;Search&lt;/a&gt;
058      &lt;a href='ShowContact.do'&gt;Contact&lt;/a&gt;
059    </PRE>
060    
061     <P>This final example uses the <tt>TTitle</tt> mechanism :
062    <PRE>
063    &lt;w:highlightCurrentPage useTitle="true"&gt;
064      &lt;a href='ShowHomePage.do'&gt;Home&lt;/a&gt;
065      &lt;a href='ShowSearch.do'&gt;Search&lt;/a&gt;
066      &lt;a href='ShowContact.do'&gt;Contact&lt;/a&gt;
067    &lt;/w:highlightCurrentPage&gt;
068    </PRE>
069     For a page with <tt>TTitle</tt> parameter as <tt>'Home'</tt>, the output of this tag 
070     would be as above:
071    <PRE>
072      Home
073      &lt;a href='ShowSearch.do'&gt;Search&lt;/a&gt;
074      &lt;a href='ShowContact.do'&gt;Contact&lt;/a&gt;
075    </PRE>
076    
077     <P><em>Nuisance restriction</em> : the <tt>href</tt> attribute must appear as the 
078     first attribute in the &lt;A&gt; tags.
079    */
080    public class HighlightCurrentPage extends TagHelper {
081    
082      /**
083       Name of the Cascading Style Sheet class used for highlighting text related to the current 
084       page (optional).
085      
086       <P>For example, the link text could be highlighted in yellow in order 
087       to make it stand out more clearly from the other links. If this attribute is not 
088       specified, then the link text is simply presented as is, with no special styling.
089      
090       @param  aCascadingStyleSheetClassName satisfies {@link Util#textHasContent(String)}.
091      */
092      public final void setStyleClass(String aCascadingStyleSheetClassName){
093        checkForContent("StyleClass", aCascadingStyleSheetClassName);
094        fHighlightClass = aCascadingStyleSheetClassName;
095      }
096      
097      /**
098       Use the <tt>TTitle</tt> request parameter passed to this page.
099       
100       <P>Optional. Default is <tt>false</tt>. If set to <tt>true</tt>, 
101       then the default mechanism is overridden, and self-linking pages will be identified 
102       by comparing the link text to the page <tt>TTitle</tt> parameter. (The <tt>TTitle</tt>
103       parameter is used in the WEB4J page templating mechanism).
104      */
105      public final void setUseTitle(Boolean aUseTitle) {
106        fUseTTitle = aUseTitle;
107      }
108      
109      /**
110       Alter any links in the tag body which refer to the current page.
111       
112       <P>See examples in the class comment.
113      */
114      @Override protected final String getEmittedText(String aOriginalBody){
115        StringBuffer result = new StringBuffer();
116        Matcher matcher = LINK_PATTERN.matcher(aOriginalBody);
117        while ( matcher.find() ) {
118          matcher.appendReplacement(result, getReplacement(matcher));
119        }
120        matcher.appendTail(result);
121        return result.toString();
122      }
123    
124      /**
125       Determine if self-linking is present, using the current URI and the link's <tt>HREF</tt> target.
126        
127       <P>Overridable default implementation that simply checks if <tt>aCurrentURI</tt> <em>ends with</em> 
128       <tt>aHrefTarget</tt>. In addition, <tt>aCurrentURI</tt> is passed through {@link EscapeChars#forHrefAmpersand(String)}
129       before the comparison is made. This ensures the text will match a valid <tt>HREF</tt> attribute. 
130      */
131      protected boolean isSelfLinkingByHref(String aCurrentURI, String aHrefTarget){
132        String currentURI = EscapeChars.forHrefAmpersand(aCurrentURI);
133        return currentURI.endsWith(aHrefTarget);
134      }
135      
136      /**
137       Determine if self-linking is present, using the link text and the <tt>TTitle</tt> request parameter.
138       
139       <P>Overridable default implementation that checks if the trimmed <tt>aLinkText</tt> 
140       (the body of the &lt;A&gt; tag) and <tt>aTTitle</tt> are the same.
141      */
142      protected boolean isSelfLinkingByTitle(String aLinkText, String aTTitle){
143        return aLinkText.trim().equals(aTTitle);
144      }
145      
146      /** Used for offline unit testing only.  */
147      void testSetCurrentURI(String aCurrentURI){
148        fTestingCurrentURI = aCurrentURI;
149      }
150      
151      /** Used for offline unit testing only. */
152      void testSetTitleParam(String aTTitleParam){
153        fTestingTitleParam = aTTitleParam;  
154      }
155      
156      // PRIVATE //
157      private String fHighlightClass;
158      private Boolean fUseTTitle = Boolean.FALSE;
159      
160      private String fTestingCurrentURI;
161      private String fTestingTitleParam;
162    
163      /**
164       Group 0 - entire link
165       Group 1 - href attribute
166       Group 2 - link text (untrimmed)
167      */
168      private static final Pattern LINK_PATTERN = Pattern.compile( 
169        "<a" +  "(?:\\s)* href=" + Regex.QUOTED_ATTR + Regex.ALL_BUT_END_OF_TAG + Regex.END_TAG + "(" + Regex.ALL_BUT_END_OF_TAG + ")" + "</a>",
170        Pattern.CASE_INSENSITIVE
171      );
172      
173      private static final int WHOLE_LINK = 0;
174      private static final int HREF_TARGET = 1;
175      private static final int LINK_TEXT = 2;
176      private static final String TITLE_PARAM = "TTitle";
177      private static final Logger fLogger = Util.getLogger(HighlightCurrentPage.class);
178      
179      private String getReplacement(Matcher aMatcher){
180        String result = aMatcher.group(WHOLE_LINK);
181        if ( isSelfLink(aMatcher) ){
182          String linkText = aMatcher.group(LINK_TEXT);
183          result = getHighlightedText(linkText);
184        }
185        return EscapeChars.forReplacementString(result);
186      }
187      
188      private String getCurrentURI(){
189        String result = null;
190        if( ! isTesting(fTestingCurrentURI) ){
191          result = (String)getRequest().getAttribute(Controller.CURRENT_URI);
192        }
193        else {
194          result = fTestingCurrentURI;
195        }
196        return result;
197      }
198      
199      private String getTitleParam(){
200        String result = null;
201        if( ! isTesting(fTestingTitleParam) ){
202          result = getRequest().getParameter(TITLE_PARAM);
203        }
204        else {
205          result = fTestingTitleParam;
206        }
207        return result;
208      }
209      
210      private boolean isSelfLink(Matcher aMatcher){
211        boolean result = false;
212        if( ! fUseTTitle ){
213          String hrefTarget = aMatcher.group(HREF_TARGET);
214          String currentURI = getCurrentURI();
215          result = isSelfLinkingByHref(currentURI, hrefTarget);
216          fLogger.finest("Is self-linking (href)? " + result);
217          fLogger.finest("Current URI " + Util.quote(currentURI) + " HREF Target : " + Util.quote(hrefTarget));
218        }
219        else {
220          String linkText = aMatcher.group(LINK_TEXT);
221          result = isSelfLinkingByTitle(linkText, getTitleParam());
222          fLogger.finest("Is self-linking (TTitle)? " + result);
223        }
224        return result;
225      }
226      
227      private String getHighlightedText(String aLinkText){
228        String result = aLinkText;
229        if( Util.textHasContent(fHighlightClass)){
230          result = "<span class=\"" + fHighlightClass + "\">" + result + "</span>";
231        }
232        return result;
233      }
234      
235      private boolean isTesting(String aTestItem){
236        return Util.textHasContent(aTestItem);
237      }
238    }