/*
* Copyright 2008-2009 Sun Microsystems, Inc. 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package org.visage.runtime.util;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.visage.runtime.util.backport.ResourceBundle;
import org.visage.runtime.util.backport.ResourceBundleEnumeration;
class VisagePropertyResourceBundle extends ResourceBundle {
private static final String CHARTAG = "@charset \"";
private static final List<String> FORMAT_VISAGEPROPERTIES
= Collections.unmodifiableList(Arrays.asList("visage.properties"));
private ConcurrentMap<String, Object> lookup;
private static Logger logger = null;
// code point literals
private static final int CRETURN = 0x000d;
private static final int NEWLINE = 0x000a;
private static final int FSLASH = 0x002f;
private static final int DQUOTE = 0x0022;
private static final int SQUOTE = 0x0027;
private static final int EQUAL = 0x003d;
private static final int BSLASH = 0x005c;
private static final int SUBST = 0xfffd;
private static final int BOM = 0xfeff;
// to be removed if we discard JDK 5 support
private static final Locale ROOTLOCALE = new Locale("");
public VisagePropertyResourceBundle(InputStream is, String resourceName)
throws IOException {
this(getReader(is), resourceName);
}
public VisagePropertyResourceBundle(Reader reader, String resourceName)
throws IOException {
lookup = new ConcurrentHashMap<String, Object>();
initialize(reader, resourceName);
}
@Override
public boolean containsKey(String key) {
if (key == null) {
throw new NullPointerException();
}
return true;
}
@Override
protected Object handleGetObject(String key) {
if (key == null) {
throw new NullPointerException();
}
return lookup.get(key);
}
@Override
public Enumeration<String> getKeys() {
ResourceBundle parent = this.parent;
return new ResourceBundleEnumeration(lookup.keySet(),
(parent != null) ? parent.getKeys() : null);
}
@Override
protected Set<String> handleKeySet() {
return lookup.keySet();
}
private void initialize(Reader reader, String resourceName) throws IOException {
BufferedReader br = new BufferedReader(reader);
int c;
int lineNum = 1;
StringBuilder sb = new StringBuilder();
String key = null;
boolean foundEqual = false;
boolean firstChar = true;
int quote = 0; // quoting character used for a literal
while ((c = getCodePoint(br)) != -1) {
switch (c) {
case CRETURN:
// normalize '\r' and "\r\n" to '\n'
br.mark(8);
if (getCodePoint(br) != NEWLINE) {
br.reset();
}
// fall through
case NEWLINE:
lineNum ++;
if (quote != 0) {
sb.appendCodePoint(NEWLINE);
}
break;
case FSLASH:
if (quote != 0) {
sb.appendCodePoint(c);
} else {
lineNum += skipComments(br, resourceName);
}
break;
case SQUOTE:
case DQUOTE:
if (quote == 0) {
if ((key == null && foundEqual) ||
(key != null && !foundEqual)) {
logPropertySyntaxError(c, lineNum, resourceName);
break;
}
// start of a literal
quote = c;
} else if (c != SQUOTE && c != DQUOTE) {
// a normal character in a literal
sb.appendCodePoint(c);
} else if (quote != c) {
// the other quote character in a literal
sb.appendCodePoint(c);
} else {
// closing of a quote
quote = 0;
if (!foundEqual && key == null) {
try {
key = convertEscapes(sb.toString());
} catch (IllegalArgumentException e) {
logPropertySyntaxError(e.getMessage(), lineNum, resourceName);
}
sb.setLength(0);
} else if (foundEqual && key != null) {
try {
lookup.put(key, convertEscapes(sb.toString()));
} catch (IllegalArgumentException e) {
logPropertySyntaxError(e.getMessage(), lineNum, resourceName);
}
sb.setLength(0);
key = null;
foundEqual = false;
} else {
logPropertySyntaxError(c, lineNum, resourceName);
}
}
break;
case EQUAL:
if (quote != 0) {
sb.appendCodePoint(c);
} else {
if (foundEqual) {
logPropertySyntaxError(c, lineNum, resourceName);
} else {
if (key == null) {
logPropertySyntaxError(c, lineNum, resourceName);
} else {
foundEqual = true;
}
}
}
break;
case BSLASH:
if (quote != 0) {
sb.appendCodePoint(c);
// append the next character no matter what
sb.appendCodePoint(getCodePoint(br));
} else {
logPropertySyntaxError(c, lineNum, resourceName);
}
break;
case BOM:
if (firstChar) {
// ignore BOM at the beginning
firstChar = false;
} else {
logPropertySyntaxError(c, lineNum, resourceName);
}
break;
default:
if (quote != 0) {
sb.appendCodePoint(c);
} else if (Character.isWhitespace(c) || c == SUBST) {
break;
} else {
logPropertySyntaxError(c, lineNum, resourceName);
}
break;
}
}
br.close();
}
private int getCodePoint(BufferedReader br) throws IOException {
int c = br.read();
if (Character.isHighSurrogate((char)c)) {
return Character.toCodePoint((char)c, (char)br.read());
} else {
return c;
}
}
private int skipComments(BufferedReader br, String resourceName) throws IOException {
int newlines = 0;
switch ((char)getCodePoint(br)) {
case '*':
// skip till we find a corresponding "*/"
while (true) {
int i = getCodePoint(br);
if ((char)i == '\n') {
newlines ++;
} else if ((char)i == '*') {
if ((char)getCodePoint(br) == '/') {
break;
}
} else if (i == -1) {
// non-closing comment causes an error
log(Level.WARNING,
"non-closing comment at the end of "+resourceName);
break;
}
}
break;
case '/':
// skip till we find a new line or end of the file
while (true) {
int i = getCodePoint(br);
if ((char)i == '\n') {
newlines ++;
break;
} else if (i == -1) {
break;
}
}
break;
}
return newlines;
}
/**
* Converts escape sequences (e.g., "\u0020") in the given <code>str</code> to
* their Unicode values and returns a String containing the converted Unicode values.
* The conversion follows the spec in JLS 3.0 3.3 Unicode Escapes and 3.10.6 Escape
* Sequences for Character and String Literals.
*
* @param str a <code>String</code> to be converted
* @return a <code>String</code> containing converted escapes.
* If the given <code>str</code> doesn't include any escape sequences,
* <code>str</code> is returned.
* @exception NullPointerException if <code>str</code> is null.
* @exception IllegalArgumentException if <code>str</code> contains any invalid
* escape sequences.
*/
private static String convertEscapes(String str) {
// Quickly check if str has any backslash.
int x= str.indexOf('\\');
if (x == -1) {
return str;
}
StringBuilder sb = new StringBuilder();
if (x != 0) {
sb.append(str, 0, x);
}
int len = str.length();
try {
while (x < len) {
char c = str.charAt(x++);
if (c != '\\') {
sb.append(c);
continue;
}
int top = x - 1;
c = str.charAt(x++);
int n = -1;
switch (c) {
case 'u':
n = 0;
for (int i = 0; i < 4; i++) {
c = str.charAt(x++);
if (('0' <= c && c <= '9') ||
('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')) {
n = (n << 4) + Character.digit(c, 16);
} else {
throw new IllegalArgumentException("illegal escape sequence '"
+ str.substring(top, x) + "'");
}
}
break;
case 'b':
n = '\b';
break;
case 't':
n = '\t';
break;
case 'n':
n = '\n';
break;
case 'f':
n = '\f';
break;
case 'r':
n = '\r';
break;
case '"':
n = '"';
break;
case '\'':
n = '\'';
break;
case '\\':
n = c;
break;
case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7':
n = Character.digit(c, 8);
char leadChar = c;
if (x < len) {
c = str.charAt(x);
if ('0' <= c && c <= '7') {
n = (n << 3) + Character.digit(c, 8);
if (++x < len) {
c = str.charAt(x);
if (leadChar <= '3' && '0' <= c && c <= '7') {
n = (n << 3) + Character.digit(c, 8);
x++;
}
}
}
}
break;
default:
throw new IllegalArgumentException("illegal escape sequence '"
+ str.substring(top, x) + "'");
}
if (n != -1) {
sb.append((char) n);
}
}
} catch (StringIndexOutOfBoundsException e) {
throw new IllegalArgumentException("illegal escape sequence: " + str);
}
return sb.toString();
}
private static Reader getReader(InputStream is) throws IOException {
Charset charset = null;
BufferedInputStream bis = new BufferedInputStream(is);
bis.mark(256);
byte[] ba = new byte[CHARTAG.length()];
if (bis.read(ba, 0, CHARTAG.length()) == ba.length) {
String possibleCharsetTag = new String(ba, "UTF-8");
if (possibleCharsetTag.equals(CHARTAG)) {
StringBuilder sb = new StringBuilder();
byte b;
boolean found = false;
while (true) {
b = (byte)bis.read();
if (b == '\r' || b == '\n') {
if (!found) {
log(Level.WARNING,
"Incorrect format in @charset tag");
}
break;
}
if (b != '"') {
sb.append((char)b);
} else {
found = true;
if ((char)bis.read() == ';') {
// conforms to the CSS encoding declaration
try {
charset = Charset.forName(sb.toString());
} catch (Exception e) {
log(Level.WARNING,
"charset '" + sb.toString() + "' was not available");
}
} else {
log(Level.WARNING,
"Incorrect format in @charset tag");
}
}
}
} else {
bis.reset();
}
}
if (charset == null) {
charset = Charset.forName("UTF-8");
}
return new InputStreamReader(bis, charset);
}
private static class VisageEchoBackResourceBundle extends ResourceBundle {
private static final Set<String> keyset = new HashSet<String>();
static final VisageEchoBackResourceBundle INSTANCE = new VisageEchoBackResourceBundle();
private VisageEchoBackResourceBundle() {
}
@Override
public boolean containsKey(String key) {
return true;
}
@Override
protected Object handleGetObject(String key) {
if (key == null) {
throw new NullPointerException();
}
return key;
}
@Override
public Enumeration<String> getKeys() {
return new ResourceBundleEnumeration(keyset, null);
}
@Override protected Set<String> handleKeySet() {
return keyset;
}
}
static class VisagePropertiesControl extends ResourceBundle.Control {
static final VisagePropertiesControl INSTANCE = new VisagePropertiesControl();
private VisagePropertiesControl() {
}
@Override
public List<String> getFormats(String baseName) {
if (baseName == null) {
throw new NullPointerException();
}
return VisagePropertyResourceBundle.FORMAT_VISAGEPROPERTIES;
}
@Override
public Locale getFallbackLocale(String baseName, Locale locale) {
if (baseName == null || locale == null) {
throw new NullPointerException();
}
return null;
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format,
ClassLoader classLoader, boolean reloadFlag)
throws IllegalAccessException, InstantiationException, IOException {
if (locale.equals(ROOTLOCALE)) {
return VisageEchoBackResourceBundle.INSTANCE;
}
String bundleName = toBundleName(baseName, locale);
ResourceBundle bundle = null;
final String resourceName = toResourceName(bundleName, "visageproperties");
final ClassLoader loader = classLoader;
final boolean reload = reloadFlag;
InputStream stream = null;
try {
stream = AccessController.doPrivileged(
new PrivilegedExceptionAction<InputStream>() {
public InputStream run() throws IOException {
InputStream is = null;
if (reload) {
URL url = loader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
// Disable caches to get fresh data for
// reloading.
connection.setUseCaches(false);
is = connection.getInputStream();
}
}
} else {
is = loader.getResourceAsStream(resourceName);
}
return is;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
if (stream != null) {
try {
bundle = new VisagePropertyResourceBundle(stream, resourceName);
} finally {
stream.close();
}
}
return bundle;
}
}
private static void logPropertySyntaxError(int c, int lineNum, String resourceName) {
logPropertySyntaxError(String.format("'%c' (U+%04X) is incorrectly placed", c, c),
lineNum, resourceName);
}
private static void logPropertySyntaxError(String message, int lineNum, String resourceName) {
logPropertySyntaxError(String.format("%s in line %d of %s", message, lineNum, resourceName));
}
private static void logPropertySyntaxError(String message) {
log(Level.WARNING, message);
throw new IllegalArgumentException(message);
}
private static void log(Level l, String msg) {
if (logger == null) {
logger = Logger.getLogger("org.visage.runtime.util.VisagePropertyResourceBundle");
}
logger.log(l, msg);
}
}