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