1 /*
   2  * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.api.scripting;
  27 
  28 import java.util.regex.Matcher;
  29 import java.util.regex.Pattern;
  30 
  31 /**
  32  * Formatter is a class to get the type conversion between javascript types and
  33  * java types for the format (sprintf) method working.
  34  *
  35  * <p>In javascript the type for numbers can be different from the format type
  36  * specifier. For format type '%d', '%o', '%x', '%X' double need to be
  37  * converted to integer. For format type 'e', 'E', 'f', 'g', 'G', 'a', 'A'
  38  * integer needs to be converted to double.
  39  *
  40  * <p>Format type "%c" and javascript string needs special handling.
  41  *
  42  * <p>The javascript date objects can be handled if they are type double (the
  43  * related javascript code will convert with Date.getTime() to double). So
  44  * double date types are converted to long.
  45  *
  46  * <p>Pattern and the logic for parameter position: java.util.Formatter
  47  *
  48  */
  49 public final class Formatter {
  50 
  51     /**
  52      * Method which converts javascript types to java types for the
  53      * String.format method (jrunscript function sprintf).
  54      *
  55      * @param format a format string
  56      * @param args arguments referenced by the format specifiers in format
  57      * @return a formatted string
  58      */
  59     public static String format(final String format, final Object[] args) {
  60         Matcher m = FS_PATTERN.matcher(format);
  61         int positionalParameter = 1;
  62 
  63         while (m.find()) {
  64             int index = index(m.group(1));
  65             boolean previous = isPreviousArgument(m.group(2));
  66             char conversion = m.group(6).charAt(0);
  67 
  68             // skip over some formats
  69             if (index < 0 || previous
  70                     || conversion == 'n' || conversion == '%') {
  71                 continue;
  72             }
  73 
  74             // index 0 here means take a positional parameter
  75             if (index == 0) {
  76                 index = positionalParameter++;
  77             }
  78 
  79             // out of index, String.format will handle
  80             if (index > args.length) {
  81                 continue;
  82             }
  83 
  84             // current argument
  85             Object arg = args[index - 1];
  86 
  87             // for date we convert double to long
  88             if (m.group(5) != null) {
  89                 // convert double to long
  90                 if (arg instanceof Double) {
  91                     args[index - 1] = ((Double) arg).longValue();
  92                 }
  93             } else {
  94                 // we have to convert some types
  95                 switch (conversion) {
  96                     case 'd':
  97                     case 'o':
  98                     case 'x':
  99                     case 'X':
 100                         if (arg instanceof Double) {
 101                             // convert double to long
 102                             args[index - 1] = ((Double) arg).longValue();
 103                         } else if (arg instanceof String
 104                                 && ((String) arg).length() > 0) {
 105                             // convert string (first character) to int
 106                             args[index - 1] = (int) ((String) arg).charAt(0);
 107                         }
 108                         break;
 109                     case 'e':
 110                     case 'E':
 111                     case 'f':
 112                     case 'g':
 113                     case 'G':
 114                     case 'a':
 115                     case 'A':
 116                         if (arg instanceof Integer) {
 117                             // convert integer to double
 118                             args[index - 1] = ((Integer) arg).doubleValue();
 119                         }
 120                         break;
 121                     case 'c':
 122                         if (arg instanceof Double) {
 123                             // convert double to integer
 124                             args[index - 1] = ((Double) arg).intValue();
 125                         } else if (arg instanceof String
 126                                 && ((String) arg).length() > 0) {
 127                             // get the first character from string
 128                             args[index - 1] = (int) ((String) arg).charAt(0);
 129                         }
 130                         break;
 131                     default:
 132                         break;
 133                 }
 134             }
 135         }
 136 
 137         return String.format(format, args);
 138     }
 139 
 140     /**
 141      * Method to parse the integer of the argument index.
 142      *
 143      * @param s
 144      * @return -1 if parsing failed, 0 if string is null, > 0 integer
 145      */
 146     private static int index(final String s) {
 147         int index = -1;
 148 
 149         if (s != null) {
 150             try {
 151                 index = Integer.parseInt(s.substring(0, s.length() - 1));
 152             } catch (NumberFormatException e) { }
 153         } else {
 154             index = 0;
 155         }
 156 
 157         return index;
 158     }
 159 
 160     /**
 161      * Method to check if a string contains '&lt;'. This is used to find out if
 162      * previous parameter is used.
 163      *
 164      * @param s
 165      * @return true if '&lt;' is in the string, else false
 166      */
 167     private static boolean isPreviousArgument(final String s) {
 168         return (s != null && s.indexOf('<') >= 0) ? true : false;
 169     }
 170 
 171     // %[argument_index$][flags][width][.precision][t]conversion
 172     private static final String formatSpecifier =
 173             "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
 174     // compiled format string
 175     private static final Pattern FS_PATTERN;
 176 
 177     static {
 178         FS_PATTERN = Pattern.compile(formatSpecifier);
 179     }
 180 }