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 <w:highlightCurrentPage styleClass="highlight">
039 <a href='ShowHomePage.do'>Home</a>
040 <a href='ShowSearch.do'>Search</a>
041 <a href='ShowContact.do'>Contact</a>
042 </w:highlightCurrentPage>
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 <span class="highlight">Home</span>
051 <a href='ShowSearch.do'>Search</a>
052 <a href='ShowContact.do'>Contact</a>
053 </PRE>
054 If the <tt>styleClass</tt> attribute is not used, then the output would simply be :
055 <PRE>
056 Home
057 <a href='ShowSearch.do'>Search</a>
058 <a href='ShowContact.do'>Contact</a>
059 </PRE>
060
061 <P>This final example uses the <tt>TTitle</tt> mechanism :
062 <PRE>
063 <w:highlightCurrentPage useTitle="true">
064 <a href='ShowHomePage.do'>Home</a>
065 <a href='ShowSearch.do'>Search</a>
066 <a href='ShowContact.do'>Contact</a>
067 </w:highlightCurrentPage>
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 <a href='ShowSearch.do'>Search</a>
074 <a href='ShowContact.do'>Contact</a>
075 </PRE>
076
077 <P><em>Nuisance restriction</em> : the <tt>href</tt> attribute must appear as the
078 first attribute in the <A> 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 <A> 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 }