1 /*
   2  * Copyright (c) 2010, 2011, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.*;
  25 import java.util.*;
  26 import java.util.regex.Matcher;
  27 import java.util.regex.Pattern;
  28 
  29 /**
  30  * Class to facilitate manipulating compiler.properties.
  31  */
  32 class MessageFile {
  33     static final Pattern emptyOrCommentPattern = Pattern.compile("( *#.*)?");
  34     static final Pattern infoPattern = Pattern.compile("# ([0-9]+: [-A-Za-z ]+, )*[0-9]+: [-A-Za-z ]+");
  35 
  36     /**
  37      * A line of text within the message file.
  38      * The lines form a doubly linked list for simple navigation.
  39      */
  40     class Line {
  41         String text;
  42         Line prev;
  43         Line next;
  44 
  45         Line(String text) {
  46             this.text = text;
  47         }
  48 
  49         boolean isEmptyOrComment() {
  50             return emptyOrCommentPattern.matcher(text).matches();
  51         }
  52 
  53         boolean isInfo() {
  54             return infoPattern.matcher(text).matches();
  55         }
  56 
  57         boolean hasContinuation() {
  58             return (next != null) && text.endsWith("\\");
  59         }
  60 
  61         Line insertAfter(String text) {
  62             Line l = new Line(text);
  63             insertAfter(l);
  64             return l;
  65         }
  66 
  67         void insertAfter(Line l) {
  68             assert l.prev == null && l.next == null;
  69             l.prev = this;
  70             l.next = next;
  71             if (next == null)
  72                 lastLine = l;
  73             else
  74                 next.prev = l;
  75             next = l;
  76         }
  77 
  78         Line insertBefore(String text) {
  79             Line l = new Line(text);
  80             insertBefore(l);
  81             return l;
  82         }
  83 
  84         void insertBefore(Line l) {
  85             assert l.prev == null && l.next == null;
  86             l.prev = prev;
  87             l.next = this;
  88             if (prev == null)
  89                 firstLine = l;
  90             else
  91                 prev.next = l;
  92             prev = l;
  93         }
  94 
  95         void remove() {
  96             if (prev == null)
  97                 firstLine = next;
  98             else
  99                 prev.next = next;
 100             if (next == null)
 101                 lastLine = prev;
 102             else
 103                 next.prev = prev;
 104             prev = null;
 105             next = null;
 106         }
 107     }
 108 
 109     /**
 110      * A message within the message file.
 111      * A message is a series of lines containing a "name=value" property,
 112      * optionally preceded by a comment describing the use of placeholders
 113      * such as {0}, {1}, etc within the property value.
 114      */
 115     static final class Message {
 116         final Line firstLine;
 117         private Info info;
 118 
 119         Message(Line l) {
 120             firstLine = l;
 121         }
 122 
 123         boolean needInfo() {
 124             Line l = firstLine;
 125             while (true) {
 126                 if (l.text.matches(".*\\{[0-9]+\\}.*"))
 127                     return true;
 128                 if (!l.hasContinuation())
 129                     return false;
 130                 l = l.next;
 131             }
 132         }
 133 
 134         Set<Integer> getPlaceholders() {
 135             Pattern p = Pattern.compile("\\{([0-9]+)\\}");
 136             Set<Integer> results = new TreeSet<Integer>();
 137             Line l = firstLine;
 138             while (true) {
 139                 Matcher m = p.matcher(l.text);
 140                 while (m.find())
 141                     results.add(Integer.parseInt(m.group(1)));
 142                 if (!l.hasContinuation())
 143                     return results;
 144                 l = l.next;
 145             }
 146         }
 147 
 148         /**
 149          * Get the Info object for this message. It may be empty if there
 150          * if no comment preceding the property specification.
 151          */
 152         Info getInfo() {
 153             if (info == null) {
 154                 Line l = firstLine.prev;
 155                 if (l != null && l.isInfo())
 156                     info = new Info(l.text);
 157                 else
 158                     info = new Info();
 159             }
 160             return info;
 161         }
 162 
 163         /**
 164          * Set the Info for this message.
 165          * If there was an info comment preceding the property specification,
 166          * it will be updated; otherwise, one will be inserted.
 167          */
 168         void setInfo(Info info) {
 169             this.info = info;
 170             Line l = firstLine.prev;
 171             if (l != null && l.isInfo())
 172                 l.text = info.toComment();
 173             else
 174                 firstLine.insertBefore(info.toComment());
 175         }
 176 
 177         /**
 178          * Get all the lines pertaining to this message.
 179          */
 180         List<Line> getLines(boolean includeAllPrecedingComments) {
 181             List<Line> lines = new ArrayList<Line>();
 182             Line l = firstLine;
 183             if (includeAllPrecedingComments) {
 184                 // scan back to find end of prev message
 185                 while (l.prev != null && l.prev.isEmptyOrComment())
 186                     l = l.prev;
 187                 // skip leading blank lines
 188                 while (l.text.isEmpty())
 189                     l = l.next;
 190             } else {
 191                 if (l.prev != null && l.prev.isInfo())
 192                     l = l.prev;
 193             }
 194 
 195             // include any preceding lines
 196             for ( ; l != firstLine; l = l.next)
 197                 lines.add(l);
 198 
 199             // include message lines
 200             for (l = firstLine; l != null && l.hasContinuation(); l = l.next)
 201                 lines.add(l);
 202             lines.add(l);
 203 
 204             // include trailing blank line if present
 205             l = l.next;
 206             if (l != null && l.text.isEmpty())
 207                 lines.add(l);
 208 
 209             return lines;
 210         }
 211     }
 212 
 213     /**
 214      * An object to represent the comment that may precede the property
 215      * specification in a Message.
 216      * The comment is modelled as a list of fields, where the fields correspond
 217      * to the placeholder values (e.g. {0}, {1}, etc) within the message value.
 218      */
 219     static final class Info {
 220         /**
 221          * An ordered set of descriptions for a placeholder value in a
 222          * message.
 223          */
 224         static class Field {
 225             boolean unused;
 226             Set<String> values;
 227             boolean listOfAny = false;
 228             boolean setOfAny = false;
 229             Field(String s) {
 230                 s = s.substring(s.indexOf(": ") + 2);
 231                 values = new LinkedHashSet<String>(Arrays.asList(s.split(" or ")));
 232                 for (String v: values) {
 233                     if (v.startsWith("list of"))
 234                         listOfAny = true;
 235                     if (v.startsWith("set of"))
 236                         setOfAny = true;
 237                 }
 238             }
 239 
 240             /**
 241              * Return true if this field logically contains all the values of
 242              * another field.
 243              */
 244             boolean contains(Field other) {
 245                 if (unused != other.unused)
 246                     return false;
 247 
 248                 for (String v: other.values) {
 249                     if (values.contains(v))
 250                         continue;
 251                     if (v.equals("null") || v.equals("string"))
 252                         continue;
 253                     if (v.equals("list") && listOfAny)
 254                         continue;
 255                     if (v.equals("set") && setOfAny)
 256                         continue;
 257                     return false;
 258                 }
 259                 return true;
 260             }
 261 
 262             /**
 263              * Merge the values of another field into this field.
 264              */
 265             void merge(Field other) {
 266                 unused |= other.unused;
 267                 values.addAll(other.values);
 268 
 269                 // cleanup unnecessary entries
 270 
 271                 if (values.contains("null") && values.size() > 1) {
 272                     // "null" is superceded by anything else
 273                     values.remove("null");
 274                 }
 275 
 276                 if (values.contains("string") && values.size() > 1) {
 277                     // "string" is superceded by anything else
 278                     values.remove("string");
 279                 }
 280 
 281                 if (values.contains("list")) {
 282                     // list is superceded by "list of ..."
 283                     for (String s: values) {
 284                         if (s.startsWith("list of ")) {
 285                             values.remove("list");
 286                             break;
 287                         }
 288                     }
 289                 }
 290 
 291                 if (values.contains("set")) {
 292                     // set is superceded by "set of ..."
 293                     for (String s: values) {
 294                         if (s.startsWith("set of ")) {
 295                             values.remove("set");
 296                             break;
 297                         }
 298                     }
 299                 }
 300 
 301                 if (other.values.contains("unused")) {
 302                     values.clear();
 303                     values.add("unused");
 304                 }
 305             }
 306 
 307             void markUnused() {
 308                 values = new LinkedHashSet<String>();
 309                 values.add("unused");
 310                 listOfAny = false;
 311                 setOfAny = false;
 312             }
 313 
 314             @Override
 315             public String toString() {
 316                 return values.toString();
 317             }
 318         }
 319 
 320         /** The fields of the Info object. */
 321         List<Field> fields = new ArrayList<Field>();
 322 
 323         Info() { }
 324 
 325         Info(String text) throws IllegalArgumentException {
 326             if (!text.startsWith("# "))
 327                 throw new IllegalArgumentException();
 328             String[] segs = text.substring(2).split(", ");
 329             fields = new ArrayList<Field>();
 330             for (String seg: segs) {
 331                 fields.add(new Field(seg));
 332             }
 333         }
 334 
 335         Info(Set<String> infos) throws IllegalArgumentException {
 336             for (String s: infos)
 337                 merge(new Info(s));
 338         }
 339 
 340         boolean isEmpty() {
 341             return fields.isEmpty();
 342         }
 343 
 344         boolean contains(Info other) {
 345             if (other.isEmpty())
 346                 return true;
 347 
 348             if (fields.size() != other.fields.size())
 349                 return false;
 350 
 351             Iterator<Field> oIter = other.fields.iterator();
 352             for (Field values: fields) {
 353                 if (!values.contains(oIter.next()))
 354                     return false;
 355             }
 356 
 357             return true;
 358         }
 359 
 360         void merge(Info other) {
 361             if (fields.isEmpty()) {
 362                 fields.addAll(other.fields);
 363                 return;
 364             }
 365 
 366             if (other.fields.size() != fields.size())
 367                 throw new IllegalArgumentException();
 368 
 369             Iterator<Field> oIter = other.fields.iterator();
 370             for (Field d: fields) {
 371                 d.merge(oIter.next());
 372             }
 373         }
 374 
 375         void markUnused(Set<Integer> used) {
 376             for (int i = 0; i < fields.size(); i++) {
 377                 if (!used.contains(i))
 378                     fields.get(i).markUnused();
 379             }
 380         }
 381 
 382         @Override
 383         public String toString() {
 384             return fields.toString();
 385         }
 386 
 387         String toComment() {
 388             StringBuilder sb = new StringBuilder();
 389             sb.append("# ");
 390             String sep = "";
 391             int i = 0;
 392             for (Field f: fields) {
 393                 sb.append(sep);
 394                 sb.append(i++);
 395                 sb.append(": ");
 396                 sep = "";
 397                 for (String s: f.values) {
 398                     sb.append(sep);
 399                     sb.append(s);
 400                     sep = " or ";
 401                 }
 402                 sep = ", ";
 403             }
 404             return sb.toString();
 405         }
 406     }
 407 
 408     Line firstLine;
 409     Line lastLine;
 410     Map<String, Message> messages = new TreeMap<String, Message>();
 411 
 412     MessageFile(File file) throws IOException {
 413         Reader in = new FileReader(file);
 414         try {
 415             read(in);
 416         } finally {
 417             in.close();
 418         }
 419     }
 420 
 421     MessageFile(Reader in) throws IOException {
 422         read(in);
 423     }
 424 
 425     final void read(Reader in) throws IOException {
 426         BufferedReader br = (in instanceof BufferedReader)
 427                 ? (BufferedReader) in
 428                 : new BufferedReader(in);
 429         String line;
 430         while ((line = br.readLine()) != null) {
 431             Line l;
 432             if (firstLine == null)
 433                 l = firstLine = lastLine = new Line(line);
 434             else
 435                 l = lastLine.insertAfter(line);
 436             if (line.startsWith("compiler.")) {
 437                 int eq = line.indexOf("=");
 438                 if (eq > 0)
 439                     messages.put(line.substring(0, eq), new Message(l));
 440             }
 441         }
 442     }
 443 
 444     void write(File file) throws IOException {
 445         Writer out = new FileWriter(file);
 446         try {
 447             write(out);
 448         } finally {
 449             out.close();
 450         }
 451     }
 452 
 453     void write(Writer out) throws IOException {
 454         BufferedWriter bw = (out instanceof BufferedWriter)
 455                 ? (BufferedWriter) out
 456                 : new BufferedWriter(out);
 457         for (Line l = firstLine; l != null; l = l.next) {
 458             bw.write(l.text);
 459             bw.write("\n"); // always use Unix line endings
 460         }
 461         bw.flush();
 462     }
 463 }