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 }