com.vityuk.ginger.loader.PropertiesLocalizationLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.vityuk.ginger.loader.PropertiesLocalizationLoader.java

Source

/*
 * Copyright 2013 Andriy Vityuk
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.vityuk.ginger.loader;

import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Chars;
import com.vityuk.ginger.PropertyResolver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Localization loader for Java properties format. Then only difference from standard Java {@link java.util.Properties}
 * is full UTF-8 support without additional conversion.
 *
 * @author Andriy Vityuk
 */
public class PropertiesLocalizationLoader implements LocalizationLoader {
    private static final CharMatcher COMMENT_MATCHER = CharMatcher.anyOf("#!");
    private static final CharMatcher LINE_SEPARATOR_MATCHER = CharMatcher.anyOf("\n\r");
    private static final CharMatcher NOT_LINE_SEPARATOR_MATCHER = LINE_SEPARATOR_MATCHER.negate();
    private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.BREAKING_WHITESPACE
            .and(NOT_LINE_SEPARATOR_MATCHER);
    private static final CharMatcher KEY_VALUE_SEPARATOR_MATCHER = CharMatcher.anyOf("=:");
    private static final CharMatcher WHITESPACE_OR_SEPARATOR_MATCHER = KEY_VALUE_SEPARATOR_MATCHER
            .or(CharMatcher.BREAKING_WHITESPACE);

    private static final Pattern MAP_KEY_PATTERN = Pattern.compile("([^\\[\\]]+)\\[([^\\[\\]]+)\\]");

    @Override
    public PropertyResolver load(InputStream inputStream) throws IOException {
        MatchingReader reader = new MatchingReader(new BufferedReader(new InputStreamReader(inputStream)));
        return load(reader);
    }

    private ResourcePropertyResolver load(MatchingReader reader) throws IOException {
        final Map<String, String> properties = Maps.newHashMap();
        final Map<String, Map<String, String>> mapProperties = Maps.newHashMap();

        while (!reader.isEndOfStream()) {
            int code = reader.peek();
            if (COMMENT_MATCHER.matches((char) code)) {
                reader.skipCharacters(NOT_LINE_SEPARATOR_MATCHER);
                continue;
            }

            reader.skipCharacters(WHITESPACE_MATCHER);
            if (!reader.isEndOfLine()) {
                String key = reader.readLineUntil(WHITESPACE_OR_SEPARATOR_MATCHER);

                reader.skipCharacters(WHITESPACE_MATCHER);

                code = reader.peek();
                if (code != -1 && KEY_VALUE_SEPARATOR_MATCHER.matches((char) code)) {
                    reader.read();
                    reader.skipCharacters(WHITESPACE_MATCHER);
                }

                String value = reader.readLineUntil(LINE_SEPARATOR_MATCHER);

                Matcher matcher = MAP_KEY_PATTERN.matcher(key);
                if (matcher.matches()) {
                    /*
                     This is map property of format: propertyKey[mapKey]=value
                    */
                    String propertyKey = matcher.group(1);
                    String mapKey = matcher.group(2);
                    Map<String, String> propertyMap = mapProperties.get(propertyKey);
                    if (propertyMap == null) {
                        propertyMap = Maps.newHashMapWithExpectedSize(4);
                        mapProperties.put(propertyKey, propertyMap);
                    }
                    propertyMap.put(mapKey, value);
                } else {
                    properties.put(key, value);
                }
            }
            reader.skipCharacters(LINE_SEPARATOR_MATCHER);
        }

        return new ResourcePropertyResolver(properties, mapProperties);
    }

    private static class MatchingReader extends Reader {
        private static final char QUOTATION_CHAR = '\\';
        private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
                'D', 'E', 'F' };

        private final BufferedReader reader;
        private int bufferedChar = -1;

        public MatchingReader(BufferedReader reader) {
            this.reader = reader;
        }

        @Override
        public int read(char[] cbuf, int off, int len) throws IOException {
            boolean hasBufferedChar = bufferedChar != -1;
            if (hasBufferedChar) {
                cbuf[off] = (char) bufferedChar;
                bufferedChar = -1;
                off++;
                len--;
            }
            int read = reader.read(cbuf, off, len);
            if (hasBufferedChar) {
                read++;
            }
            return read;
        }

        public int peek() throws IOException {
            if (bufferedChar == -1) {
                bufferedChar = read();
            }
            return bufferedChar;
        }

        public boolean isEndOfStream() throws IOException {
            return peek() == -1;
        }

        private boolean isEndOfLine() throws IOException {
            int code = peek();
            if (code == -1) {
                return true;
            }
            return LINE_SEPARATOR_MATCHER.matches((char) code);
        }

        public void skipCharacters(CharMatcher charMatcher) throws IOException {
            for (;;) {
                int code = peek();
                if (code == -1 || !charMatcher.matches((char) code)) {
                    return;
                }
                read();
            }
        }

        public String readLineUntil(CharMatcher charMatcher) throws IOException {
            final StringBuilder sb = new StringBuilder();
            for (;;) {
                int code = peek();
                if (code == -1 || charMatcher.matches((char) code)) {
                    break;
                }

                int quotedCode = readQuotedCharacter();
                if (quotedCode != -1) {
                    sb.append((char) quotedCode);
                }
            }
            return sb.toString();
        }

        private int readQuotedCharacter() throws IOException {
            int code = read();
            if (code == -1) {
                return -1;
            }

            char ch = (char) code;
            if (ch != QUOTATION_CHAR) {
                return ch;
            }

            return readCharacter();
        }

        private int readCharacter() throws IOException {
            int code = read();
            if (code == -1) {
                return -1;
            }

            char ch = (char) code;
            switch (ch) {
            case 'u':
                return readEscapedUnicodeCharacter();
            case '\r':
                if (peek() == '\n') {
                    // skip it
                    read();
                }
            case '\n':
                skipCharacters(WHITESPACE_MATCHER);
                return -1;
            case 't':
                return '\t';
            case 'n':
                return '\n';
            case 'r':
                return '\r';
            default:
                return ch;
            }
        }

        private char readEscapedUnicodeCharacter() throws IOException {
            int result = 0;
            for (int i = 0; i < 4; i++) {
                int code = read();
                if (code == -1) {
                    throw new IllegalArgumentException("Malformed \\uxxxx encoding.");
                }
                char ch = (char) code;
                int digit = Chars.indexOf(HEX_CHARS, Character.toUpperCase(ch));
                if (digit == -1) {
                    throw new IllegalArgumentException("Malformed \\uxxxx encoding.");
                }

                result = result * 16 + digit;
            }
            return (char) result;
        }

        @Override
        public void close() throws IOException {
            reader.close();
        }
    }

    private static class ResourcePropertyResolver implements PropertyResolver {
        private final Map<String, String> properties;
        private final Map<String, Map<String, String>> mapProperties;

        private static final Splitter ARRAY_SPLITTER = Splitter.on(',').trimResults();

        public ResourcePropertyResolver(Map<String, String> properties,
                Map<String, Map<String, String>> mapProperties) {
            this.properties = properties;
            this.mapProperties = mapProperties;
        }

        @Override
        public String getString(String key) {
            return get(key);
        }

        @Override
        public Boolean getBoolean(String key) {
            String value = get(key);
            return value == null ? null : Boolean.valueOf(value);
        }

        @Override
        public Integer getInteger(String key) {
            String value = get(key);
            return value == null ? null : Integer.valueOf(value);
        }

        @Override
        public Long getLong(String key) {
            String value = get(key);
            return value == null ? null : Long.valueOf(value);
        }

        @Override
        public Float getFloat(String key) {
            String value = get(key);
            return value == null ? null : Float.valueOf(value);
        }

        @Override
        public Double getDouble(String key) {
            String value = get(key);
            return value == null ? null : Double.valueOf(value);
        }

        @Override
        public List<String> getStringList(String key) {
            String value = get(key);
            return value == null ? null
                    : Collections.unmodifiableList(Lists.newArrayList(ARRAY_SPLITTER.split(value)));
        }

        @Override
        public Map<String, String> getStringMap(String key) {
            return mapProperties.get(key);
        }

        private String get(String key) {
            Preconditions.checkNotNull(key);
            return properties.get(key);
        }
    }
}