001    package hirondelle.web4j.ui.translate;
002    
003    import java.util.Locale;
004    import java.util.logging.Logger;
005    import java.util.regex.*;
006    
007    import hirondelle.web4j.BuildImpl;
008    import hirondelle.web4j.request.LocaleSource;
009    import hirondelle.web4j.ui.tag.TagHelper;
010    import hirondelle.web4j.util.Util;
011    import hirondelle.web4j.util.Regex;
012    import hirondelle.web4j.util.EscapeChars;
013    
014    /**
015     Custom tag for translating 
016     <a href="http://www.w3.org/TR/html4/struct/global.html#adef-title"><tt>TITLE</tt></a>,  
017     <a href="http://www.w3.org/TR/html4/struct/objects.html#adef-alt"><tt>ALT</tt></a> 
018     and submit-button 
019     <a href="http://www.w3.org/TR/html4/interact/forms.html#adef-value-INPUT"><tt>VALUE</tt></a>
020     attributes in markup.
021     
022     <P><span class="highlight">By using this custom tag <em>once</em> in a template JSP, 
023     it is often possible to translate <em>all</em> of the <tt>TITLE</tt>, 
024     <tt>ALT</tt>, and submit-button <tt>VALUE</tt> attributes appearing in an entire application.</span>
025     
026     <P>The <tt>VALUE</tt> attribute is translated only for <tt>SUBMIT</tt> controls. (This 
027     <tt>VALUE</tt> attribute isn't really a tooltip, of course : it is rendered as the button text. 
028     For this custom tag, the distinction is not very important.)
029     
030     <P>The <tt>TITLE</tt> attribute applies to a large number of HTML tags, and
031     the <tt>ALT</tt> attribute applies to several tags. In both cases, these 
032     items generate pop-up "tool tips", which are visible to the end user. If the application is 
033     multilingual, then they require translation.
034     
035     <P>This custom tag accepts HTML markup for its body, and will do a search and 
036     replace on its content, replacing the values of all <tt>TITLE</tt>, <tt>ALT</tt> and 
037     submit-button <tt>VALUE</tt> attributes (that have visible content) with translated values. 
038     The translations are provided by the configured implementations of 
039     {@link Translator} and {@link LocaleSource}.
040    */
041    public final class Tooltips extends TagHelper {
042      
043      /**
044       Control the escaping of special characters.
045        
046       <P>By default, this tag will escape any special characters appearing in the 
047       <tt>TITLE</tt> or <tt>ALT</tt> attribute, using {@link EscapeChars#forHTML(String)}. 
048       To override this default behaviour, set this value to <tt>false</tt>. 
049      */
050      public void setEscapeChars(boolean aValue){
051        fEscapeChars = aValue;
052      }
053      
054      /**
055       Scan the body of this tag, and translate the values of all <tt>TITLE</tt>, <tt>ALT</tt>, 
056       and submit-button <tt>VALUE</tt> attributes.
057       
058      <P>Uses the configured {@link Translator} and {@link LocaleSource}.
059       
060       <P>In addition, this method uses {@link EscapeChars#forHTML(String)} to ensure that 
061       any special characters are escaped. This behavior can be overridden using {@link #setEscapeChars(boolean)}. 
062      */
063      @Override protected String getEmittedText(String aOriginalBody) {
064        //fLogger.finest("Original Body: " + aOriginalBody);
065        String result = translateTooltips(aOriginalBody);
066        result = translateSubmitButtons(result);
067        //fLogger.finest("TranslateTooltips translated : " + result.toString());
068        return result;
069      }
070    
071      /**
072       Pattern which returns the value of <tt>TITLE</tt> and <tt>ALT</tt> attributes (including any quotes), 
073       as group 2, which is to be translated. 
074      */
075      static final Pattern TOOLTIP = Pattern.compile(
076        "(<[^>]* (?:title=|alt=))" + Regex.ATTR_VALUE + "([^>]*>)",  
077        Pattern.CASE_INSENSITIVE
078      );
079      
080      /**
081       Pattern which returns the value of <tt>VALUE</tt> attribute (including any quotes) of a <tt>SUBMIT</tt>
082       control, as group 2, which is to be translated.
083       
084       <P>Small nuisance restriction : the general order of items must follow this style, where <tt>type</tt>
085       and <tt>value</tt> precede all other attributes, and <tt>type</tt> precedes <tt>value</tt>:
086       <PRE>
087        &lt;input type="submit" value="Add" [any other attributes go here]&gt;
088       </PRE>
089      */
090      static final Pattern SUBMIT_BUTTON = Pattern.compile(
091        "(<input(?:\\s)* (?:type=\"submit\"|type='submit'|type=submit)(?:\\s)* value=)" + Regex.ATTR_VALUE + "([^>]*>)", 
092        Pattern.CASE_INSENSITIVE
093      );
094      
095      // PRIVATE //
096      
097      private static final Logger fLogger = Util.getLogger(Tooltips.class);
098      private boolean fEscapeChars = true;
099      private LocaleSource fLocaleSource = BuildImpl.forLocaleSource();
100      private Translator fTranslator = BuildImpl.forTranslator();
101      
102      private String translateTooltips(String aInput){
103        return scanAndReplace(TOOLTIP, aInput);
104      }
105      
106      private String translateSubmitButtons(String aInput){
107        return scanAndReplace(SUBMIT_BUTTON, aInput);
108      }
109      
110      private String scanAndReplace(Pattern aPattern, String aInput){
111        StringBuffer result = new StringBuffer();
112        Matcher matcher = aPattern.matcher(aInput);
113        while ( matcher.find() ) {
114          matcher.appendReplacement(result, getReplacement(matcher));
115        }
116        matcher.appendTail(result);
117        return result.toString();
118      }
119      
120      private String getReplacement(Matcher aMatcher){
121        String result = null;
122        String baseText = Util.removeQuotes(aMatcher.group(Regex.SECOND_GROUP));
123        if (Util.textHasContent(baseText)){
124          String start = aMatcher.group(Regex.FIRST_GROUP);
125          String end = aMatcher.group(Regex.THIRD_GROUP);
126          Locale locale = fLocaleSource.get(getRequest());
127          String translatedText = fTranslator.get(baseText, locale);
128          if ( fEscapeChars ){
129            translatedText = EscapeChars.forHTML(translatedText);
130          }
131          result = start + Util.quote(translatedText) + end;
132        }
133        else {
134          result = aMatcher.group(Regex.ENTIRE_MATCH);
135        }
136        result = EscapeChars.forReplacementString(result);
137        fLogger.finest("TITLE/ALT base Text: " + Util.quote(baseText) + " has replacement text : " + Util.quote(result));
138        return result;
139      }
140    }