blob: cbf731c34aeafcbe148dfccc72fb3d0e1ff95489 [file] [log] [blame]
/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class to facilitate manipulating compiler.properties.
*/
class MessageFile {
static final Pattern emptyOrCommentPattern = Pattern.compile("( *#.*)?");
static final Pattern typePattern = Pattern.compile("[-\\\\'A-Z\\.a-z ]+( \\([A-Za-z 0-9]+\\))?");
static final Pattern infoPattern = Pattern.compile(String.format("# ([0-9]+: %s, )*[0-9]+: %s",
typePattern.pattern(), typePattern.pattern()));
/**
* A line of text within the message file.
* The lines form a doubly linked list for simple navigation.
*/
class Line {
String text;
Line prev;
Line next;
Line(String text) {
this.text = text;
}
boolean isEmptyOrComment() {
return emptyOrCommentPattern.matcher(text).matches();
}
boolean isInfo() {
return infoPattern.matcher(text).matches();
}
boolean hasContinuation() {
return (next != null) && text.endsWith("\\");
}
Line insertAfter(String text) {
Line l = new Line(text);
insertAfter(l);
return l;
}
void insertAfter(Line l) {
assert l.prev == null && l.next == null;
l.prev = this;
l.next = next;
if (next == null)
lastLine = l;
else
next.prev = l;
next = l;
}
Line insertBefore(String text) {
Line l = new Line(text);
insertBefore(l);
return l;
}
void insertBefore(Line l) {
assert l.prev == null && l.next == null;
l.prev = prev;
l.next = this;
if (prev == null)
firstLine = l;
else
prev.next = l;
prev = l;
}
void remove() {
if (prev == null)
firstLine = next;
else
prev.next = next;
if (next == null)
lastLine = prev;
else
next.prev = prev;
prev = null;
next = null;
}
}
/**
* A message within the message file.
* A message is a series of lines containing a "name=value" property,
* optionally preceded by a comment describing the use of placeholders
* such as {0}, {1}, etc within the property value.
*/
static final class Message {
final Line firstLine;
private Info info;
Message(Line l) {
firstLine = l;
}
boolean needInfo() {
Line l = firstLine;
while (true) {
if (l.text.matches(".*\\{[0-9]+\\}.*"))
return true;
if (!l.hasContinuation())
return false;
l = l.next;
}
}
Set<Integer> getPlaceholders() {
Pattern p = Pattern.compile("\\{([0-9]+)\\}");
Set<Integer> results = new TreeSet<Integer>();
Line l = firstLine;
while (true) {
Matcher m = p.matcher(l.text);
while (m.find())
results.add(Integer.parseInt(m.group(1)));
if (!l.hasContinuation())
return results;
l = l.next;
}
}
/**
* Get the Info object for this message. It may be empty if there
* if no comment preceding the property specification.
*/
Info getInfo() {
if (info == null) {
Line l = firstLine.prev;
if (l != null && l.isInfo())
info = new Info(l.text);
else
info = new Info();
}
return info;
}
/**
* Set the Info for this message.
* If there was an info comment preceding the property specification,
* it will be updated; otherwise, one will be inserted.
*/
void setInfo(Info info) {
this.info = info;
Line l = firstLine.prev;
if (l != null && l.isInfo())
l.text = info.toComment();
else
firstLine.insertBefore(info.toComment());
}
/**
* Get all the lines pertaining to this message.
*/
List<Line> getLines(boolean includeAllPrecedingComments) {
List<Line> lines = new ArrayList<Line>();
Line l = firstLine;
if (includeAllPrecedingComments) {
// scan back to find end of prev message
while (l.prev != null && l.prev.isEmptyOrComment())
l = l.prev;
// skip leading blank lines
while (l.text.isEmpty())
l = l.next;
} else {
if (l.prev != null && l.prev.isInfo())
l = l.prev;
}
// include any preceding lines
for ( ; l != firstLine; l = l.next)
lines.add(l);
// include message lines
for (l = firstLine; l != null && l.hasContinuation(); l = l.next)
lines.add(l);
lines.add(l);
// include trailing blank line if present
l = l.next;
if (l != null && l.text.isEmpty())
lines.add(l);
return lines;
}
}
/**
* An object to represent the comment that may precede the property
* specification in a Message.
* The comment is modelled as a list of fields, where the fields correspond
* to the placeholder values (e.g. {0}, {1}, etc) within the message value.
*/
static final class Info {
/**
* An ordered set of descriptions for a placeholder value in a
* message.
*/
static class Field {
boolean unused;
Set<String> values;
boolean listOfAny = false;
boolean setOfAny = false;
Field(String s) {
s = s.substring(s.indexOf(": ") + 2);
values = new LinkedHashSet<String>(Arrays.asList(s.split(" or ")));
for (String v: values) {
if (v.startsWith("list of"))
listOfAny = true;
if (v.startsWith("set of"))
setOfAny = true;
}
}
/**
* Return true if this field logically contains all the values of
* another field.
*/
boolean contains(Field other) {
if (unused != other.unused)
return false;
for (String v: other.values) {
if (values.contains(v))
continue;
if (v.equals("null") || v.equals("string"))
continue;
if (v.equals("list") && listOfAny)
continue;
if (v.equals("set") && setOfAny)
continue;
return false;
}
return true;
}
/**
* Merge the values of another field into this field.
*/
void merge(Field other) {
unused |= other.unused;
values.addAll(other.values);
// cleanup unnecessary entries
if (values.contains("null") && values.size() > 1) {
// "null" is superceded by anything else
values.remove("null");
}
if (values.contains("string") && values.size() > 1) {
// "string" is superceded by anything else
values.remove("string");
}
if (values.contains("list")) {
// list is superceded by "list of ..."
for (String s: values) {
if (s.startsWith("list of ")) {
values.remove("list");
break;
}
}
}
if (values.contains("set")) {
// set is superceded by "set of ..."
for (String s: values) {
if (s.startsWith("set of ")) {
values.remove("set");
break;
}
}
}
if (other.values.contains("unused")) {
values.clear();
values.add("unused");
}
}
void markUnused() {
values = new LinkedHashSet<String>();
values.add("unused");
listOfAny = false;
setOfAny = false;
}
@Override
public String toString() {
return values.toString();
}
}
/** The fields of the Info object. */
List<Field> fields = new ArrayList<Field>();
Info() { }
Info(String text) throws IllegalArgumentException {
if (!text.startsWith("# "))
throw new IllegalArgumentException();
String[] segs = text.substring(2).split(", ");
fields = new ArrayList<Field>();
for (String seg: segs) {
fields.add(new Field(seg));
}
}
Info(Set<String> infos) throws IllegalArgumentException {
for (String s: infos)
merge(new Info(s));
}
boolean isEmpty() {
return fields.isEmpty();
}
boolean contains(Info other) {
if (other.isEmpty())
return true;
if (fields.size() != other.fields.size())
return false;
Iterator<Field> oIter = other.fields.iterator();
for (Field values: fields) {
if (!values.contains(oIter.next()))
return false;
}
return true;
}
void merge(Info other) {
if (fields.isEmpty()) {
fields.addAll(other.fields);
return;
}
if (other.fields.size() != fields.size())
throw new IllegalArgumentException();
Iterator<Field> oIter = other.fields.iterator();
for (Field d: fields) {
d.merge(oIter.next());
}
}
void markUnused(Set<Integer> used) {
for (int i = 0; i < fields.size(); i++) {
if (!used.contains(i))
fields.get(i).markUnused();
}
}
@Override
public String toString() {
return fields.toString();
}
String toComment() {
StringBuilder sb = new StringBuilder();
sb.append("# ");
String sep = "";
int i = 0;
for (Field f: fields) {
sb.append(sep);
sb.append(i++);
sb.append(": ");
sep = "";
for (String s: f.values) {
sb.append(sep);
sb.append(s);
sep = " or ";
}
sep = ", ";
}
return sb.toString();
}
}
Line firstLine;
Line lastLine;
Map<String, Message> messages = new TreeMap<String, Message>();
MessageFile(File file) throws IOException {
Reader in = new FileReader(file);
try {
read(in);
} finally {
in.close();
}
}
MessageFile(Reader in) throws IOException {
read(in);
}
final void read(Reader in) throws IOException {
BufferedReader br = (in instanceof BufferedReader)
? (BufferedReader) in
: new BufferedReader(in);
String line;
while ((line = br.readLine()) != null) {
Line l;
if (firstLine == null)
l = firstLine = lastLine = new Line(line);
else
l = lastLine.insertAfter(line);
if (line.startsWith("compiler.")) {
int eq = line.indexOf("=");
if (eq > 0)
messages.put(line.substring(0, eq), new Message(l));
}
}
}
void write(File file) throws IOException {
Writer out = new FileWriter(file);
try {
write(out);
} finally {
out.close();
}
}
void write(Writer out) throws IOException {
BufferedWriter bw = (out instanceof BufferedWriter)
? (BufferedWriter) out
: new BufferedWriter(out);
for (Line l = firstLine; l != null; l = l.next) {
bw.write(l.text);
bw.write("\n"); // always use Unix line endings
}
bw.flush();
}
}