MessageFile.java revision 2776:aa568700edd1
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
24import java.io.*;
25import java.util.*;
26import java.util.regex.Matcher;
27import java.util.regex.Pattern;
28
29/**
30 * Class to facilitate manipulating compiler.properties.
31 */
32class 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
47        Line(String text) {
48            this.text = text;
49        }
50
51        boolean isEmptyOrComment() {
52            return emptyOrCommentPattern.matcher(text).matches();
53        }
54
55        boolean isInfo() {
56            return infoPattern.matcher(text).matches();
57        }
58
59        boolean hasContinuation() {
60            return (next != null) && text.endsWith("\\");
61        }
62
63        Line insertAfter(String text) {
64            Line l = new Line(text);
65            insertAfter(l);
66            return l;
67        }
68
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
80        Line insertBefore(String text) {
81            Line l = new Line(text);
82            insertBefore(l);
83            return l;
84        }
85
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
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
121        Message(Line l) {
122            firstLine = l;
123        }
124
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
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         */
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         */
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         */
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;
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             */
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             */
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
309            void markUnused() {
310                values = new LinkedHashSet<String>();
311                values.add("unused");
312                listOfAny = false;
313                setOfAny = false;
314            }
315
316            @Override
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
325        Info() { }
326
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
337        Info(Set<String> infos) throws IllegalArgumentException {
338            for (String s: infos)
339                merge(new Info(s));
340        }
341
342        boolean isEmpty() {
343            return fields.isEmpty();
344        }
345
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
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
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
385        public String toString() {
386            return fields.toString();
387        }
388
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
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
423    MessageFile(Reader in) throws IOException {
424        read(in);
425    }
426
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
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
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