Coverage Report - org.apache.maven.doxia.util.DoxiaUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
DoxiaUtils
77%
53/68
91%
86/94
5,333
 
 1  
 package org.apache.maven.doxia.util;
 2  
 
 3  
 /*
 4  
  * Licensed to the Apache Software Foundation (ASF) under one
 5  
  * or more contributor license agreements.  See the NOTICE file
 6  
  * distributed with this work for additional information
 7  
  * regarding copyright ownership.  The ASF licenses this file
 8  
  * to you under the Apache License, Version 2.0 (the
 9  
  * "License"); you may not use this file except in compliance
 10  
  * with the License.  You may obtain a copy of the License at
 11  
  *
 12  
  *   http://www.apache.org/licenses/LICENSE-2.0
 13  
  *
 14  
  * Unless required by applicable law or agreed to in writing,
 15  
  * software distributed under the License is distributed on an
 16  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 17  
  * KIND, either express or implied.  See the License for the
 18  
  * specific language governing permissions and limitations
 19  
  * under the License.
 20  
  */
 21  
 
 22  
 import java.awt.image.BufferedImage;
 23  
 
 24  
 import java.io.File;
 25  
 import java.io.IOException;
 26  
 import java.io.UnsupportedEncodingException;
 27  
 
 28  
 import java.net.URL;
 29  
 
 30  
 import java.text.ParseException;
 31  
 import java.text.ParsePosition;
 32  
 import java.text.SimpleDateFormat;
 33  
 
 34  
 import java.util.Date;
 35  
 import java.util.Locale;
 36  
 
 37  
 import javax.imageio.ImageIO;
 38  
 
 39  
 import javax.swing.text.MutableAttributeSet;
 40  
 
 41  
 import org.apache.maven.doxia.sink.SinkEventAttributeSet;
 42  
 
 43  
 /**
 44  
  * General Doxia utility methods. The methods in this class should not assume
 45  
  * any specific Doxia module or document format.
 46  
  *
 47  
  * @author ltheussl
 48  
  * @since 1.1
 49  
  * @version $Id: DoxiaUtils.java 1185112 2011-10-17 11:33:00Z ltheussl $
 50  
  */
 51  
 public class DoxiaUtils
 52  
 {
 53  
     private static final int MINUS_ONE = 0xFF;
 54  
 
 55  
     /**
 56  
      * Checks if the given string corresponds to an internal link,
 57  
      * ie it is a link to an anchor within the same document.
 58  
      * If link is not null, then exactly one of the three methods
 59  
      * {@link #isInternalLink(java.lang.String)}, {@link #isExternalLink(java.lang.String)} and
 60  
      * {@link #isLocalLink(java.lang.String)} will return true.
 61  
      *
 62  
      * @param link The link to check. Not null.
 63  
      * @return True if the link starts with "#".
 64  
      *
 65  
      * @throws NullPointerException if link is null.
 66  
      *
 67  
      * @see #isExternalLink(String)
 68  
      * @see #isLocalLink(String)
 69  
      */
 70  
     public static boolean isInternalLink( final String link )
 71  
     {
 72  14
         return link.startsWith( "#" );
 73  
     }
 74  
 
 75  
     /**
 76  
      * Checks if the given string corresponds to an external URI,
 77  
      * ie is not a link within the same document nor a relative link
 78  
      * to another document (a local link) of the same site.
 79  
      * If link is not null, then exactly one of the three methods
 80  
      * {@link #isInternalLink(java.lang.String)}, {@link #isExternalLink(java.lang.String)} and
 81  
      * {@link #isLocalLink(java.lang.String)} will return true.
 82  
      *
 83  
      * @param link The link to check. Not null.
 84  
      *
 85  
      * @return True if the link (ignoring case) starts with either "http:/",
 86  
      * "https:/", "ftp:/", "mailto:", "file:/", or contains the string "://".
 87  
      * Note that Windows style separators "\" are not allowed
 88  
      * for URIs, see  http://www.ietf.org/rfc/rfc2396.txt , section 2.4.3.
 89  
      *
 90  
      * @throws NullPointerException if link is null.
 91  
      *
 92  
      * @see #isInternalLink(String)
 93  
      * @see #isLocalLink(String)
 94  
      */
 95  
     public static boolean isExternalLink( final String link )
 96  
     {
 97  54
         String text = link.toLowerCase( Locale.ENGLISH );
 98  
 
 99  54
         return ( text.startsWith( "http:/" ) || text.startsWith( "https:/" )
 100  
             || text.startsWith( "ftp:/" ) || text.startsWith( "mailto:" )
 101  
             || text.startsWith( "file:/" ) || text.contains( "://" ) );
 102  
     }
 103  
 
 104  
     /**
 105  
      * Checks if the given string corresponds to a relative link to another document
 106  
      * within the same site, ie it is neither an {@link #isInternalLink(String) internal}
 107  
      * nor an {@link #isExternalLink(String) external} link.
 108  
      * If link is not null, then exactly one of the three methods
 109  
      * {@link #isInternalLink(java.lang.String)}, {@link #isExternalLink(java.lang.String)} and
 110  
      * {@link #isLocalLink(java.lang.String)} will return true.
 111  
      *
 112  
      * @param link The link to check. Not null.
 113  
      *
 114  
      * @return True if the link is neither an external nor an internal link.
 115  
      *
 116  
      * @throws NullPointerException if link is null.
 117  
      *
 118  
      * @see #isExternalLink(String)
 119  
      * @see #isInternalLink(String)
 120  
      */
 121  
     public static boolean isLocalLink( final String link )
 122  
     {
 123  10
         return ( !isExternalLink( link ) && !isInternalLink( link ) );
 124  
     }
 125  
 
 126  
     /**
 127  
      * Construct a valid Doxia id.
 128  
      *
 129  
      * <p>
 130  
      *   This method is equivalent to {@link #encodeId(java.lang.String, boolean) encodeId( id, false )}.
 131  
      * </p>
 132  
      *
 133  
      * @param id The id to be encoded.
 134  
      *      May be null in which case null is returned.
 135  
      *
 136  
      * @return The trimmed and encoded id, or null if id is null.
 137  
      *
 138  
      * @see #encodeId(java.lang.String, boolean)
 139  
      */
 140  
     public static String encodeId( final String id )
 141  
     {
 142  26
         return encodeId( id, false );
 143  
     }
 144  
 
 145  
     /**
 146  
      * Construct a valid Doxia id.
 147  
      *
 148  
      * <p>
 149  
      *   A valid Doxia id obeys the same constraints as an HTML ID or NAME token.
 150  
      *   According to the <a href="http://www.w3.org/TR/html4/types.html#type-name">
 151  
      *   HTML 4.01 specification section 6.2 SGML basic types</a>:
 152  
      * </p>
 153  
      * <p>
 154  
      *   <i>ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
 155  
      *   followed by any number of letters, digits ([0-9]), hyphens ("-"),
 156  
      *   underscores ("_"), colons (":"), and periods (".").</i>
 157  
      * </p>
 158  
      * <p>
 159  
      *   According to <a href="http://www.w3.org/TR/xhtml1/#C_8">XHTML 1.0
 160  
      *   section C.8. Fragment Identifiers</a>:
 161  
      * </p>
 162  
      * <p>
 163  
      *   <i>When defining fragment identifiers to be backward-compatible, only
 164  
      *   strings matching the pattern [A-Za-z][A-Za-z0-9:_.-]* should be used.</i>
 165  
      * </p>
 166  
      * <p>
 167  
      *   To achieve this we need to convert the <i>id</i> String. Two conversions
 168  
      *   are necessary and one is done to get prettier ids:
 169  
      * </p>
 170  
      * <ol>
 171  
      *   <li>Remove whitespace at the start and end before starting to process</li>
 172  
      *   <li>If the first character is not a letter, prepend the id with the letter 'a'</li>
 173  
      *   <li>Any spaces are replaced with an underscore '_'</li>
 174  
      *   <li>
 175  
      *     Any characters not matching the above pattern are either dropped,
 176  
      *     or replaced according to the rules specified in the
 177  
      *     <a href="http://www.w3.org/TR/html4/appendix/notes.html#non-ascii-chars">HTML specs</a>.
 178  
      *   </li>
 179  
      * </ol>
 180  
      * <p>
 181  
      *   For letters, the case is preserved in the conversion.
 182  
      * </p>
 183  
      *
 184  
      * <p>
 185  
      * Here are some examples:
 186  
      * </p>
 187  
      * <pre>
 188  
      * DoxiaUtils.encodeId( null )        = null
 189  
      * DoxiaUtils.encodeId( "" )          = "a"
 190  
      * DoxiaUtils.encodeId( "  " )        = "a"
 191  
      * DoxiaUtils.encodeId( " _ " )       = "a_"
 192  
      * DoxiaUtils.encodeId( "1" )         = "a1"
 193  
      * DoxiaUtils.encodeId( "1anchor" )   = "a1anchor"
 194  
      * DoxiaUtils.encodeId( "_anchor" )   = "a_anchor"
 195  
      * DoxiaUtils.encodeId( "a b-c123 " ) = "a_b-c123"
 196  
      * DoxiaUtils.encodeId( "   anchor" ) = "anchor"
 197  
      * DoxiaUtils.encodeId( "myAnchor" )  = "myAnchor"
 198  
      * </pre>
 199  
      *
 200  
      * @param id The id to be encoded.
 201  
      *      May be null in which case null is returned.
 202  
      * @param chop true if non-ASCII characters should be ignored.
 203  
      * If false, any non-ASCII characters will be replaced as specified above.
 204  
      *
 205  
      * @return The trimmed and encoded id, or null if id is null.
 206  
      * If id is not null, the return value is guaranteed to be a valid Doxia id.
 207  
      *
 208  
      * @see #isValidId(java.lang.String)
 209  
      *
 210  
      * @since 1.1.1
 211  
      */
 212  
     public static String encodeId( final String id, final boolean chop )
 213  
     {
 214  120
         if ( id == null )
 215  
         {
 216  4
             return null;
 217  
         }
 218  
 
 219  116
         final String idd = id.trim();
 220  116
         int length = idd.length();
 221  
 
 222  116
         if ( length == 0 )
 223  
         {
 224  8
             return "a";
 225  
         }
 226  
 
 227  108
         StringBuilder buffer = new StringBuilder( length );
 228  
 
 229  538
         for ( int i = 0; i < length; ++i )
 230  
         {
 231  430
             char c = idd.charAt( i );
 232  
 
 233  430
             if ( ( i == 0 ) && ( !isAsciiLetter( c ) ) )
 234  
             {
 235  22
                 buffer.append( 'a' );
 236  
             }
 237  
 
 238  430
             if ( c == ' ' )
 239  
             {
 240  4
                 buffer.append( '_' );
 241  
             }
 242  426
             else if ( isAsciiLetter( c ) || isAsciiDigit( c ) || ( c == '-' ) || ( c == '_' ) || ( c == ':' )
 243  
                             || ( c == '.' ) )
 244  
             {
 245  412
                 buffer.append( c );
 246  
             }
 247  14
             else if ( !chop )
 248  
             {
 249  
                 byte[] bytes;
 250  
 
 251  
                 try
 252  
                 {
 253  6
                     bytes = String.valueOf( c ).getBytes( "UTF8" );
 254  
                 }
 255  0
                 catch ( UnsupportedEncodingException cannotHappen )
 256  
                 {
 257  0
                     bytes = new byte[0];
 258  6
                 }
 259  
 
 260  16
                 for ( int j = 0; j < bytes.length; ++j )
 261  
                 {
 262  10
                     String hex = byteToHex( bytes[j] );
 263  
 
 264  10
                     buffer.append( '%' );
 265  
 
 266  10
                     if ( hex.length() == 1 )
 267  
                     {
 268  0
                         buffer.append( '0' );
 269  
                     }
 270  
 
 271  10
                     buffer.append( hex );
 272  
                 }
 273  
             }
 274  
         }
 275  
 
 276  108
         return buffer.toString();
 277  
     }
 278  
 
 279  
     /**
 280  
      * Convert a byte to it's hexadecimal equivalent.
 281  
      *
 282  
      * @param b the byte value.
 283  
      * @return the result of Integer.toHexString( b & 0xFF ).
 284  
      *
 285  
      * @since 1.1.1
 286  
      */
 287  
     public static String byteToHex( final byte b )
 288  
     {
 289  38
         return Integer.toHexString( b & MINUS_ONE );
 290  
     }
 291  
 
 292  
     /**
 293  
      * Determines if the specified text is a valid id according to the rules
 294  
      * laid out in {@link #encodeId(String)}.
 295  
      *
 296  
      * @param text The text to be tested.
 297  
      *      May be null in which case false is returned.
 298  
      *
 299  
      * @return <code>true</code> if the text is a valid id, otherwise <code>false</code>.
 300  
      *
 301  
      * @see #encodeId(String)
 302  
      */
 303  
     public static boolean isValidId( final String text )
 304  
     {
 305  74
         if ( text == null || text.length() == 0 )
 306  
         {
 307  8
             return false;
 308  
         }
 309  
 
 310  196
         for ( int i = 0; i < text.length(); ++i )
 311  
         {
 312  170
             char c = text.charAt( i );
 313  
 
 314  170
             if ( isAsciiLetter( c ) )
 315  
             {
 316  112
                 continue;
 317  
             }
 318  
 
 319  58
             if ( ( i == 0 ) || ( c == ' ' ) || ( !isAsciiDigit( c ) && c != '-' && c != '_' && c != ':' && c != '.' ) )
 320  
             {
 321  40
                 return false;
 322  
             }
 323  
         }
 324  
 
 325  26
         return true;
 326  
     }
 327  
 
 328  2
     private static final SimpleDateFormat DATE_PARSER = new SimpleDateFormat( "", Locale.ENGLISH );
 329  2
     private static final ParsePosition DATE_PARSE_POSITION = new ParsePosition( 0 );
 330  2
     private static final String[] DATE_PATTERNS = new String[]
 331  
     {
 332  
         "yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy", "dd.MM.yyyy", "dd MMM yyyy",
 333  
         "dd MMM. yyyy", "MMMM yyyy", "MMM. dd, yyyy", "MMM. yyyy", "MMMM dd, yyyy",
 334  
         "MMM d, ''yy", "MMM. ''yy", "MMMM ''yy"
 335  
     };
 336  
 
 337  
     /**
 338  
      * <p>Parses a string representing a date by trying different date patterns.</p>
 339  
      *
 340  
      * <p>The following date patterns are tried (in the given order):</p>
 341  
      *
 342  
      * <pre>"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy", "dd.MM.yyyy", "dd MMM yyyy",
 343  
      *  "dd MMM. yyyy", "MMMM yyyy", "MMM. dd, yyyy", "MMM. yyyy", "MMMM dd, yyyy",
 344  
      *  "MMM d, ''yy", "MMM. ''yy", "MMMM ''yy"</pre>
 345  
      *
 346  
      * <p>A parse is only sucessful if it parses the whole of the input string.
 347  
      * If no parse patterns match, a ParseException is thrown.</p>
 348  
      *
 349  
      * <p>As a special case, the strings <code>"today"</code> and <code>"now"</code>
 350  
      * (ignoring case) return the current date.</p>
 351  
      *
 352  
      * @param str the date to parse, not null.
 353  
      *
 354  
      * @return the parsed date, or the current date if the input String (ignoring case) was
 355  
      *      <code>"today"</code> or <code>"now"</code>.
 356  
      *
 357  
      * @throws ParseException if no pattern matches.
 358  
      * @throws NullPointerException if str is null.
 359  
      *
 360  
      * @since 1.1.1.
 361  
      */
 362  
     public static Date parseDate( final String str )
 363  
             throws ParseException
 364  
     {
 365  36
         if ( "today".equalsIgnoreCase( str ) || "now".equalsIgnoreCase( str ) )
 366  
         {
 367  4
             return new Date();
 368  
         }
 369  
 
 370  250
         for ( int i = 0; i < DATE_PATTERNS.length; i++ )
 371  
         {
 372  248
             DATE_PARSER.applyPattern( DATE_PATTERNS[i] );
 373  248
             DATE_PARSE_POSITION.setIndex( 0 );
 374  248
             final Date date = DATE_PARSER.parse( str, DATE_PARSE_POSITION );
 375  
 
 376  248
             if ( date != null && DATE_PARSE_POSITION.getIndex() == str.length() )
 377  
             {
 378  30
                 return date;
 379  
             }
 380  
         }
 381  
 
 382  2
         throw new ParseException( "Unable to parse date: " + str, -1 );
 383  
     }
 384  
 
 385  
       //
 386  
      // private
 387  
     //
 388  
 
 389  
     private static boolean isAsciiLetter( final char c )
 390  
     {
 391  704
         return ( ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) );
 392  
     }
 393  
 
 394  
     private static boolean isAsciiDigit( final char c )
 395  
     {
 396  162
         return ( c >= '0' && c <= '9' );
 397  
     }
 398  
 
 399  
     /**
 400  
      * Determine width and height of an image. If successful, the returned SinkEventAttributes
 401  
      * contain width and height attribute keys whose values are the width and height of the image (as a String).
 402  
      *
 403  
      * @param logo a String containing either a URL or a path to an image file. Not null.
 404  
      *
 405  
      * @return a set of SinkEventAttributes, or null if no ImageReader was found to read the image.
 406  
      *
 407  
      * @throws java.io.IOException if an error occurs during reading.
 408  
      * @throws NullPointerException if logo is null.
 409  
      *
 410  
      * @since 1.1.1
 411  
      */
 412  
     public static MutableAttributeSet getImageAttributes( final String logo )
 413  
             throws IOException
 414  
     {
 415  0
         BufferedImage img = null;
 416  
 
 417  0
         if ( isExternalLink( logo ) )
 418  
         {
 419  0
             img = ImageIO.read( new URL( logo ) );
 420  
         }
 421  
         else
 422  
         {
 423  0
             img = ImageIO.read( new File( logo ) );
 424  
         }
 425  
 
 426  0
         if ( img == null )
 427  
         {
 428  0
             return null;
 429  
         }
 430  
 
 431  0
         MutableAttributeSet atts = new SinkEventAttributeSet();
 432  0
         atts.addAttribute( SinkEventAttributeSet.WIDTH, Integer.toString( img.getWidth() ) );
 433  0
         atts.addAttribute( SinkEventAttributeSet.HEIGHT, Integer.toString( img.getHeight() ) );
 434  
         // add other attributes?
 435  
 
 436  0
         return atts;
 437  
     }
 438  
 
 439  
     private DoxiaUtils()
 440  0
     {
 441  
         // utility class
 442  0
     }
 443  
 }