All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dbflute.helper.jprop.JavaPropertiesReader Maven / Gradle / Ivy

There is a newer version: 1.2.8
Show newest version
/*
 * Copyright 2014-2015 the original author or authors.
 *
 * 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 org.dbflute.helper.jprop;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.dbflute.helper.jprop.exception.JavaPropertiesImplicitOverrideException;
import org.dbflute.helper.jprop.exception.JavaPropertiesLonelyOverrideException;
import org.dbflute.helper.jprop.exception.JavaPropertiesReadFailureException;
import org.dbflute.helper.jprop.exception.JavaPropertiesStreamNotFoundException;
import org.dbflute.helper.message.ExceptionMessageBuilder;
import org.dbflute.util.DfCollectionUtil;
import org.dbflute.util.DfCollectionUtil.AccordingToOrderIdExtractor;
import org.dbflute.util.DfCollectionUtil.AccordingToOrderResource;
import org.dbflute.util.DfReflectionUtil;
import org.dbflute.util.Srl;
import org.dbflute.util.Srl.ScopeInfo;

/**
 * @author jflute
 * @since 1.0.1 (2012/12/15 Saturday)
 */
public class JavaPropertiesReader {

    // ===================================================================================
    //                                                                          Definition
    //                                                                          ==========
    public static final String OVERRIDE_ANNOTATION = "@Override";
    public static final String SECURE_ANNOTATION = "@Secure";

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    // -----------------------------------------------------
    //                                                 Basic
    //                                                 -----
    protected final String _title;
    protected final JavaPropertiesStreamProvider _streamProvider;

    // -----------------------------------------------------
    //                                                Option
    //                                                ------
    protected final Map _extendsProviderMap = newLinkedHashMapSized(4);
    protected boolean _checkImplicitOverride;
    protected String _streamEncoding; // used if set
    protected boolean _useNonNumberVariable;

    // -----------------------------------------------------
    //                                            Reflection
    //                                            ----------
    protected Method _convertMethod; // cached
    protected boolean _convertMethodNotFound;
    protected final Properties _reflectionProperties = new Properties();

    // ===================================================================================
    //                                                                         Constructor
    //                                                                         ===========
    public JavaPropertiesReader(String title, JavaPropertiesStreamProvider streamProvider) {
        _title = title;
        _streamProvider = streamProvider;
    }

    // -----------------------------------------------------
    //                                                Option
    //                                                ------
    public JavaPropertiesReader extendsProperties(String title, JavaPropertiesStreamProvider noArgLambda) {
        if (_extendsProviderMap.containsKey(title)) {
            String msg = "The argument 'title' has already been registered:";
            msg = msg + " title=" + title + " registered=" + _extendsProviderMap.keySet();
            throw new IllegalArgumentException(msg);
        }
        _extendsProviderMap.put(title, noArgLambda);
        return this;
    }

    public JavaPropertiesReader checkImplicitOverride() {
        _checkImplicitOverride = true;
        return this;
    }

    public JavaPropertiesReader encodeAsUTF8() {
        _streamEncoding = "UTF-8";
        return this;
    }

    public JavaPropertiesReader encodeAs(String encoding) {
        _streamEncoding = encoding;
        return this;
    }

    public JavaPropertiesReader useNonNumberVariable() {
        _useNonNumberVariable = true;
        return this;
    }

    // ===================================================================================
    //                                                                                Read
    //                                                                                ====
    public JavaPropertiesResult read() {
        final List propertyList = newArrayList();
        final List duplicateKeyList = newArrayList();
        final Map keyCommentMap = readKeyCommentMap(duplicateKeyList);
        final Properties prop = readPlainProperties();
        final List keyList = orderKeyList(prop, keyCommentMap);
        for (String key : keyList) {
            final String value = prop.getProperty(key);
            final String comment = keyCommentMap.get(key);

            final JavaPropertiesProperty property = new JavaPropertiesProperty(key, value);

            final String defName = Srl.replace(key, ".", "_").toUpperCase();
            property.setDefName(defName);

            final String camelizedName = Srl.camelize(defName);
            property.setCamelizedName(camelizedName);
            property.setCapCamelName(Srl.initCap(camelizedName));
            property.setUncapCamelName(Srl.initUncap(camelizedName));

            final List variableScopeList = analyzeVariableScopeList(value);
            final List variableNumberList = DfCollectionUtil.newArrayListSized(variableScopeList.size());
            final List variableStringList = DfCollectionUtil.newArrayListSized(variableScopeList.size());
            reflectToVariableList(key, variableScopeList, variableNumberList, variableStringList);
            property.setVariableNumberList(variableNumberList);
            property.setVariableStringList(variableStringList);
            final List variableArgNameList = prepareVariableArgNameList(variableStringList);
            property.setVariableArgNameList(variableArgNameList);
            property.setVariableArgDef(buildVariableArgDef(variableArgNameList));
            property.setVariableArgSet(buildVariableArgSet(variableArgNameList));
            property.setComment(comment);
            if (containsSecureAnnotation(property)) {
                property.toBeSecure();
            }

            propertyList.add(property);
        }
        return prepareResult(prop, propertyList, duplicateKeyList);
    }

    protected List analyzeVariableScopeList(final String value) {
        final List variableScopeList = newArrayList();
        {
            final List scopeList;
            if (Srl.is_NotNull_and_NotTrimmedEmpty(value)) {
                scopeList = extractVariableScopeList(value);
            } else {
                scopeList = DfCollectionUtil.emptyList();
            }
            for (ScopeInfo scopeInfo : scopeList) {
                final String content = scopeInfo.getContent();
                if (isUnsupportedVariableContent(content)) {
                    continue;
                }
                variableScopeList.add(scopeInfo);
            }
        }
        return variableScopeList;
    }

    protected List extractVariableScopeList(String value) {
        return Srl.extractScopeList(value, "{", "}"); // e.g. {0} is for {1}.
    }

    protected boolean isUnsupportedVariableContent(String content) {
        if (content.contains(" ")) {
            return true;
        }
        if (!_useNonNumberVariable) {
            try {
                Integer.valueOf(content);
            } catch (NumberFormatException ignored) { // e.g. {A} is for {B}
                return true;
            }
        }
        return false;
    }

    protected void reflectToVariableList(String key, List variableScopeList, List variableNumberList,
            List variableStringList) {
        for (ScopeInfo scopeInfo : variableScopeList) {
            final String content = scopeInfo.getContent();
            final Integer variableNumber = valueOfVariableNumber(key, content);
            if (variableNumber != null) { // for non number option
                variableNumberList.add(variableNumber);
            }
            variableStringList.add(content);
        }
    }

    protected List prepareVariableArgNameList(List variableStringList) {
        final List variableArgNameList = DfCollectionUtil.newArrayListSized(variableStringList.size());
        for (Object name : variableStringList) {
            variableArgNameList.add(doBuildVariableArgName(name));
        }
        return variableArgNameList;
    }

    protected String doBuildVariableArgName(Object name) {
        return startsWithNumberVariable(name) ? ("arg" + name) : name.toString();
    }

    protected boolean startsWithNumberVariable(Object variable) {
        return "1234567890".contains(Srl.cut(variable.toString(), 1)); // sorry, simple logic
    }

    protected boolean containsSecureAnnotation(JavaPropertiesProperty property) {
        final String comment = property.getComment();
        return comment != null && Srl.containsIgnoreCase(comment, SECURE_ANNOTATION);
    }

    // -----------------------------------------------------
    //                                             Order Key
    //                                             ---------
    protected List orderKeyList(Properties prop, final Map keyCommentMap) {
        final List orderedList = newArrayList(prop.keySet());
        final AccordingToOrderResource resource = new AccordingToOrderResource();
        resource.setIdExtractor(new AccordingToOrderIdExtractor() {
            public String extractId(Object element) {
                return (String) element;
            }
        });
        resource.setOrderedUniqueIdList(newArrayList(keyCommentMap.keySet()));
        DfCollectionUtil.orderAccordingTo(orderedList, resource);
        final List keyList = newArrayList();
        for (Object keyObj : orderedList) {
            keyList.add((String) keyObj);
        }
        return keyList;
    }

    // -----------------------------------------------------
    //                                        Prepare Result
    //                                        --------------
    protected JavaPropertiesResult prepareResult(Properties prop, List propertyList, List duplicateKeyList) {
        final JavaPropertiesResult propResult;
        if (!_extendsProviderMap.isEmpty()) {
            final JavaPropertiesReader extendsReader = createExtendsReader();
            final JavaPropertiesResult extendsPropResult = extendsReader.read();
            final List mergedList = mergeExtendsPropResult(propertyList, extendsPropResult);
            propResult = newJavaPropertiesResult(prop, duplicateKeyList, extendsPropResult, mergedList);
        } else {
            propResult = newJavaPropertiesResult(prop, propertyList, duplicateKeyList);
        }
        return propResult;
    }

    protected JavaPropertiesResult newJavaPropertiesResult(Properties prop, List duplicateKeyList,
            JavaPropertiesResult extendsPropResult, List mergedList) {
        return new JavaPropertiesResult(prop, mergedList, duplicateKeyList, extendsPropResult);
    }

    protected JavaPropertiesResult newJavaPropertiesResult(Properties prop, List propertyList,
            List duplicateKeyList) {
        return new JavaPropertiesResult(prop, propertyList, duplicateKeyList);
    }

    protected JavaPropertiesReader createExtendsReader() {
        final Map providerMap = newLinkedHashMap(_extendsProviderMap);
        final Entry firstEntry = providerMap.entrySet().iterator().next();
        final String firstKey = firstEntry.getKey();
        final JavaPropertiesStreamProvider firstProvider = firstEntry.getValue();
        final JavaPropertiesReader extendsReader = newJavaPropertiesReader(firstKey, firstProvider);
        providerMap.remove(firstKey);
        for (Entry entry : providerMap.entrySet()) { // next extends
            extendsReader.extendsProperties(entry.getKey(), entry.getValue());
        }
        if (_checkImplicitOverride) {
            extendsReader.checkImplicitOverride();
        }
        return extendsReader;
    }

    protected JavaPropertiesReader newJavaPropertiesReader(String firstKey, JavaPropertiesStreamProvider firstProvider) {
        return new JavaPropertiesReader(firstKey, firstProvider);
    }

    // ===================================================================================
    //                                                                               Merge
    //                                                                               =====
    protected List mergeExtendsPropResult(List propertyList,
            JavaPropertiesResult extendsPropResult) {
        final List extendsPropertyList = extendsPropResult.getPropertyList();
        for (JavaPropertiesProperty property : extendsPropertyList) {
            property.toBeExtends();
        }
        final Map extendsPropertyMap = toPropertyMap(extendsPropertyList);
        for (JavaPropertiesProperty property : propertyList) {
            final String propertyKey = property.getPropertyKey();
            if (extendsPropertyMap.containsKey(propertyKey)) {
                property.toBeOverride();
                checkImplicitOverride(property);
                final JavaPropertiesProperty extendsProperty = extendsPropertyMap.get(propertyKey);
                inheritSecure(property, extendsProperty);
                inheritComment(property, extendsProperty);
            } else {
                checkLonelyOverride(property);
            }
        }
        final Set mergedPropertySet = DfCollectionUtil.newLinkedHashSet(propertyList);
        mergedPropertySet.addAll(extendsPropertyMap.values()); // merge (add if not exists)
        return DfCollectionUtil.newArrayList(mergedPropertySet);
    }

    protected Map toPropertyMap(List propertyList) {
        final Map propertyMap = DfCollectionUtil.newLinkedHashMap();
        for (JavaPropertiesProperty property : propertyList) {
            propertyMap.put(property.getPropertyKey(), property);
        }
        return propertyMap;
    }

    protected void checkImplicitOverride(JavaPropertiesProperty property) {
        if (_checkImplicitOverride && !containsOverrideAnnotation(property)) {
            throwJavaPropertiesImplicitOverrideException(property);
        }
    }

    protected void checkLonelyOverride(JavaPropertiesProperty property) {
        if (_checkImplicitOverride && containsOverrideAnnotation(property)) {
            throwJavaPropertiesLonelyOverrideException(property);
        }
    }

    protected boolean containsOverrideAnnotation(JavaPropertiesProperty property) {
        final String comment = property.getComment();
        return comment != null && Srl.containsIgnoreCase(comment, OVERRIDE_ANNOTATION);
    }

    protected void inheritSecure(JavaPropertiesProperty property, JavaPropertiesProperty extendsProperty) {
        if (extendsProperty.isSecure()) {
            property.toBeSecure(); // inherit
        }
    }

    protected void inheritComment(JavaPropertiesProperty property, JavaPropertiesProperty extendsProperty) {
        final String comment = property.getComment();
        if (hasCommentIgnoreAnnotation(comment)) {
            return;
        }
        // only annotations or empty
        final String extendsPureComment = extractPureComment(extendsProperty.getComment());
        if (Srl.is_Null_or_TrimmedEmpty(extendsPureComment)) {
            return;
        }
        final String baseComment = Srl.is_NotNull_and_NotTrimmedEmpty(comment) ? comment + " " : "";
        property.setComment(baseComment + extendsPureComment); // inherit
    }

    protected boolean hasCommentIgnoreAnnotation(String comment) {
        if (Srl.is_Null_or_TrimmedEmpty(comment)) {
            return false;
        }
        final String replaced = extractPureComment(comment);
        return Srl.is_NotNull_and_NotTrimmedEmpty(replaced);
    }

    protected String extractPureComment(String comment) {
        if (Srl.is_Null_or_TrimmedEmpty(comment)) {
            return comment;
        }
        final Map fromToMap = new HashMap();
        fromToMap.put(OVERRIDE_ANNOTATION, "");
        fromToMap.put(SECURE_ANNOTATION, "");
        return Srl.replaceBy(comment, fromToMap);
    }

    protected void throwJavaPropertiesImplicitOverrideException(JavaPropertiesProperty property) {
        final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
        br.addNotice("Found the implicit override property.");
        br.addItem("Advice");
        br.addElement("The property overrides the inherited property.");
        br.addElement("Do you want to override it? Or is it your mistake?");
        br.addElement("If you override it, set the annotation " + OVERRIDE_ANNOTATION + " in the property.");
        br.addElement("For example:");
        br.addElement("  # " + OVERRIDE_ANNOTATION);
        br.addElement("  foo.bar.prop = abc");
        br.addItem("Properties");
        br.addElement(_title);
        br.addItem("Implicit Override Property");
        br.addElement(property.getPropertyKey());
        br.addElement(property.getPropertyValue());
        final String msg = br.buildExceptionMessage();
        throw new JavaPropertiesImplicitOverrideException(msg);
    }

    protected void throwJavaPropertiesLonelyOverrideException(JavaPropertiesProperty property) {
        final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
        br.addNotice("Found the lonely override property.");
        br.addItem("Advice");
        br.addElement("The property does not override any inherited property");
        br.addElement("but the property have the annotation " + OVERRIDE_ANNOTATION + ".");
        br.addElement("Remove the annotation or fix the mistake of the property key.");
        br.addItem("Properties");
        br.addElement(_title);
        br.addItem("Lonely Override Property");
        br.addElement(property.getPropertyKey());
        br.addElement(property.getPropertyValue());
        final String msg = br.buildExceptionMessage();
        throw new JavaPropertiesLonelyOverrideException(msg);
    }

    // ===================================================================================
    //                                                                         Read Helper
    //                                                                         ===========
    protected Map readKeyCommentMap(List duplicateKeyList) {
        final Map keyCommentMap = DfCollectionUtil.newLinkedHashMap();
        final String encoding = "UTF-8"; // because properties normally cannot have double bytes
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(preparePropFileStream(), encoding));
            String previousComment = null;
            while (true) {
                final String line = br.readLine();
                if (line == null) {
                    break;
                }
                final String ltrimmedLine = Srl.ltrim(line);
                if (ltrimmedLine.startsWith("# ")) { // comment lines
                    final String commentCandidate = Srl.substringFirstRear(ltrimmedLine, "#").trim();
                    if (ltrimmedLine.contains("=")) { // you cannot contain equal mark in comment
                        previousComment = null; // e.g. #foo.bar.qux = value (comment out???)
                    } else {
                        if (!ltrimmedLine.trim().equals("#")) { // not sharp lonely
                            previousComment = commentCandidate; // 99% comment
                        }
                    }
                    continue;
                }
                // key value here
                if (!ltrimmedLine.contains("=")) { // what's this? (no way)
                    continue;
                }
                final String key = Srl.substringFirstFront(ltrimmedLine, "=").trim();
                if (keyCommentMap.containsKey(key)) {
                    duplicateKeyList.add(key);
                    keyCommentMap.remove(key); // remove existing key for order and override
                }
                keyCommentMap.put(key, loadConvert(previousComment));
                previousComment = null;
            }
        } catch (IOException e) {
            throwJavaPropertiesReadFailureException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException ignored) {}
            }
        }
        return keyCommentMap;
    }

    protected Properties readPlainProperties() {
        final Properties prop = new Properties();
        InputStream ins = null;
        try {
            ins = preparePropFileStream();
            loadProperties(prop, ins);
        } catch (IOException e) {
            throwJavaPropertiesReadFailureException(e);
        } finally {
            if (ins != null) {
                try {
                    ins.close();
                } catch (IOException ignored) {}
            }
        }
        return prop;
    }

    protected InputStream preparePropFileStream() throws IOException {
        final InputStream stream = _streamProvider.provideStream();
        if (stream == null) {
            throwJavaPropertiesStreamNotFoundException();
        }
        return stream;
    }

    protected void loadProperties(Properties prop, InputStream ins) throws IOException {
        if (_streamEncoding != null) {
            BufferedReader br = null;
            try {
                br = new BufferedReader(new InputStreamReader(ins, _streamEncoding));
                prop.load(br);
            } finally {
                if (br != null) {
                    try {
                        br.close();
                    } catch (IOException ignored) {}
                }
            }
        } else {
            prop.load(ins);
        }
    }

    protected void throwJavaPropertiesStreamNotFoundException() {
        final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
        br.addNotice("Not found the steram for the properties file.");
        br.addItem("Advice");
        br.addElement("The stream provider should not return null but null returned.");
        br.addElement("Make sure your resource path for the properties.");
        br.addItem("Properties");
        br.addElement(_title);
        br.addItem("Stream Provider");
        br.addElement(_streamProvider);
        final String msg = br.buildExceptionMessage();
        throw new JavaPropertiesStreamNotFoundException(msg);
    }

    protected void throwJavaPropertiesReadFailureException(IOException e) {
        final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
        br.addNotice("Failed to read the properties file.");
        br.addItem("Properties");
        br.addElement(_title);
        br.addItem("IOException");
        br.addElement(e.getClass().getName());
        br.addElement(e.getMessage());
        final String msg = br.buildExceptionMessage();
        throw new JavaPropertiesReadFailureException(msg, e);
    }

    // ===================================================================================
    //                                                                     Variable Helper
    //                                                                     ===============
    protected Integer valueOfVariableNumber(String key, String content) {
        try {
            return Integer.valueOf(content);
        } catch (NumberFormatException e) {
            if (_useNonNumberVariable) {
                return null;
            }
            String msg = "The NOT-number variable was found: provider=" + _streamProvider + " key=" + key;
            throw new IllegalStateException(msg, e);
        }
    }

    protected String buildVariableArgDef(List variableArgNameList) {
        final StringBuilder sb = new StringBuilder();
        for (String name : variableArgNameList) {
            if (sb.length() > 0) {
                sb.append(", ");
            }
            doBuildVariableArgStringDef(sb, name);
        }
        return sb.toString();
    }

    protected void doBuildVariableArgStringDef(StringBuilder sb, String variableName) {
        sb.append("String ").append(variableName); // java style
    }

    protected String buildVariableArgSet(List variableArgNameList) {
        final StringBuilder sb = new StringBuilder();
        for (String name : variableArgNameList) {
            if (sb.length() > 0) {
                sb.append(", ");
            }
            sb.append(name);
        }
        return sb.toString();
    }

    // ===================================================================================
    //                                                                     Unicode Convert
    //                                                                     ===============
    protected String loadConvert(String expression) {
        if (expression == null) {
            return null;
        }
        final Method method = getConvertMethod();
        if (method == null) {
            return expression;
        }
        final char[] in = expression.toCharArray();
        final Object[] args = new Object[] { in, 0, expression.length(), new char[] {} };
        return (String) DfReflectionUtil.invoke(method, _reflectionProperties, args);
    }

    protected Method getConvertMethod() {
        if (_convertMethod != null) {
            return _convertMethod;
        }
        if (_convertMethodNotFound) {
            return null;
        }
        final Class[] argTypes = new Class[] { char[].class, int.class, int.class, char[].class };
        _convertMethod = DfReflectionUtil.getWholeMethod(Properties.class, "loadConvert", argTypes);
        if (_convertMethod == null) {
            _convertMethodNotFound = true;
        } else {
            _convertMethod.setAccessible(true);
        }
        return _convertMethod;
    }

    // ===================================================================================
    //                                                                      General Helper
    //                                                                      ==============
    protected  ArrayList newArrayList() {
        return DfCollectionUtil.newArrayList();
    }

    protected  ArrayList newArrayList(Collection elements) {
        return DfCollectionUtil.newArrayList(elements);
    }

    protected  LinkedHashMap newLinkedHashMap(Map map) {
        return DfCollectionUtil.newLinkedHashMap(map);
    }

    protected  LinkedHashMap newLinkedHashMapSized(int size) {
        return DfCollectionUtil.newLinkedHashMapSized(size);
    }
}