org.nuxeo.launcher.commons.text.TextTemplate.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.launcher.commons.text.TextTemplate.java

Source

/*
 * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     Nuxeo - initial API and implementation
 *     bstefanescu, jcarsique
 *
 */

package org.nuxeo.launcher.commons.text;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * Text template processing.
 * <p>
 * Copy files or directories replacing parameters matching pattern '${[a-zA-Z_0-9\-\.]+}' with values from a {@link Map}
 * (deprecated) or a {@link Properties}.
 * <p>
 * Method {@link #setTextParsingExtensions(String)} allow to set list of files being processed when using
 * {@link #processDirectory(File, File)} or #pro, others are simply copied.
 *
 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
 */
public class TextTemplate {

    private static final Log log = LogFactory.getLog(TextTemplate.class);

    private static final Pattern PATTERN = Pattern.compile("(?<!\\$)\\$\\{([a-zA-Z_0-9\\-\\.]+)\\}");

    private static final int MAX_RECURSION_LEVEL = 10;

    private final Properties vars;

    private Properties processedVars;

    private boolean trim = false;

    private List<String> plainTextExtensions;

    private List<String> freemarkerExtensions = new ArrayList<String>();

    private Configuration freemarkerConfiguration = null;

    private Map<String, Object> freemarkerVars = null;

    private boolean extensionsContainsDot = false;

    public boolean isTrim() {
        return trim;
    }

    /**
     * Set to true in order to trim invisible characters (spaces) from values.
     */
    public void setTrim(boolean trim) {
        this.trim = trim;
    }

    public TextTemplate() {
        vars = new Properties();
        processedVars = new Properties();
    }

    /**
     * @deprecated prefer use of {@link #TextTemplate(Properties)}
     */
    @Deprecated
    public TextTemplate(Map<String, String> vars) {
        this.vars = new Properties();
        this.vars.putAll(vars);
        processedVars = preprocessVars(this.vars);
    }

    /**
     * @param vars Properties containing keys and values for template processing
     */
    public TextTemplate(Properties vars) {
        this.vars = vars;
        processedVars = preprocessVars(this.vars);
    }

    /**
     * @deprecated prefer use of {@link #getVariables()} then {@link Properties} .load()
     */
    @Deprecated
    public void setVariables(Map<String, String> vars) {
        this.vars.putAll(vars);
        processedVars = preprocessVars(this.vars);
    }

    public void setVariable(String name, String value) {
        vars.setProperty(name, value);
        processedVars = preprocessVars(vars);
    }

    public String getVariable(String name) {
        return vars.getProperty(name);
    }

    public Properties getVariables() {
        return vars;
    }

    public Properties preprocessVars(Properties unprocessedVars) {
        Properties newVars = new Properties(unprocessedVars);
        boolean doneProcessing = false;
        int recursionLevel = 0;
        while (!doneProcessing) {
            doneProcessing = true;
            @SuppressWarnings("rawtypes")
            Enumeration newVarsEnum = newVars.propertyNames();
            while (newVarsEnum.hasMoreElements()) {
                String newVarsKey = (String) newVarsEnum.nextElement();
                String newVarsValue = newVars.getProperty(newVarsKey);
                Matcher m = PATTERN.matcher(newVarsValue);
                StringBuffer sb = new StringBuffer();
                while (m.find()) {
                    String embeddedVar = m.group(1);
                    String value = newVars.getProperty(embeddedVar);
                    if (value != null) {
                        if (trim) {
                            value = value.trim();
                        }
                        String escapedValue = Matcher.quoteReplacement(value);
                        m.appendReplacement(sb, escapedValue);
                    }
                }
                m.appendTail(sb);
                String replacementValue = sb.toString();
                if (!replacementValue.equals(newVarsValue)) {
                    doneProcessing = false;
                    newVars.put(newVarsKey, replacementValue);
                }
            }
            recursionLevel++;
            // Avoid infinite replacement loops
            if ((!doneProcessing) && (recursionLevel > MAX_RECURSION_LEVEL)) {
                break;
            }
        }
        return unescape(newVars);
    }

    protected Properties unescape(Properties props) {
        // unescape variables
        for (Object key : props.keySet()) {
            props.put(key, unescape((String) props.get(key)));
        }
        return props;
    }

    protected String unescape(String value) {
        return value.replaceAll("\\$\\$", "\\$");
    }

    public String processText(CharSequence text) {
        Matcher m = PATTERN.matcher(text);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String var = m.group(1);
            String value = getVariable(var);
            if (value != null) {
                if (trim) {
                    value = value.trim();
                }

                // process again the value if it still contains variable
                // to replace
                String oldValue = value;
                int recursionLevel = 0;
                while (!(value = processText(oldValue)).equals(oldValue)) {
                    oldValue = value;
                    recursionLevel++;
                    // Avoid infinite replacement loops
                    if (recursionLevel > MAX_RECURSION_LEVEL) {
                        break;
                    }
                }

                // Allow use of backslash and dollars characters
                String valueL = Matcher.quoteReplacement(value);
                m.appendReplacement(sb, valueL);
            }
        }
        m.appendTail(sb);
        return unescape(sb.toString());
    }

    public String processText(InputStream in) throws IOException {
        String text = IOUtils.toString(in, "UTF-8");
        return processText(text);
    }

    public void processText(InputStream is, OutputStream os) throws IOException {
        String text = IOUtils.toString(is, "UTF-8");
        text = processText(text);
        os.write(text.getBytes("UTF-8"));
    }

    public void initFreeMarker() {
        // Initialize FreeMarker
        freemarkerConfiguration = new Configuration();
        freemarkerConfiguration.setObjectWrapper(new DefaultObjectWrapper());
        // Initialize data model
        freemarkerVars = new HashMap<String, Object>();
        @SuppressWarnings("rawtypes")
        Enumeration processedEnum = processedVars.propertyNames();
        Map<String, Object> currentMap;
        String currentString;
        while (processedEnum.hasMoreElements()) {
            String key = (String) processedEnum.nextElement();
            String value = processedVars.getProperty(key);
            String[] keyparts = key.split("\\.");
            currentMap = freemarkerVars;
            currentString = "";
            boolean setKeyVal = true;
            for (int i = 0; i < (keyparts.length - 1); i++) {
                currentString = currentString + (currentString.equals("") ? "" : ".") + keyparts[i];
                if (!currentMap.containsKey(keyparts[i])) {
                    Map<String, Object> nextMap = new HashMap<String, Object>();
                    currentMap.put(keyparts[i], nextMap);
                    currentMap = nextMap;
                } else {
                    if (currentMap.get(keyparts[i]) instanceof Map<?, ?>) {
                        currentMap = (Map<String, Object>) currentMap.get(keyparts[i]);
                    } else {
                        // silently ignore known conflicts in java properties
                        if (!key.startsWith("java.vendor")) {
                            log.warn("FreeMarker templates: " + currentString + " is already defined - " + key
                                    + " will not be available in the data model.");
                        }
                        setKeyVal = false;
                        break;
                    }
                }
            }
            if (setKeyVal) {
                currentMap.put(keyparts[keyparts.length - 1], value);
            }
        }
    }

    public void processFreemarker(File in, File out) throws IOException, TemplateException {
        if (freemarkerConfiguration == null) {
            initFreeMarker();
        }
        freemarkerConfiguration.setDirectoryForTemplateLoading(in.getParentFile());
        Template nxtpl = freemarkerConfiguration.getTemplate(in.getName());
        try (Writer writer = new EscapeVariableFilter(new FileWriter(out))) {
            nxtpl.process(freemarkerVars, writer);
        }
    }

    protected static class EscapeVariableFilter extends FilterWriter {

        protected static final int DOLLAR_SIGN = "$".codePointAt(0);

        protected int last;

        public EscapeVariableFilter(Writer out) {
            super(out);
        }

        public @Override void write(int b) throws IOException {
            if (b == DOLLAR_SIGN && last == DOLLAR_SIGN) {
                return;
            }
            out.write(b);
            last = b;
        }

        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {
            for (int i = 0; i < len; ++i) {
                write(cbuf[off + i]);
            }
        }

        @Override
        public void write(char[] cbuf) throws IOException {
            write(cbuf, 0, cbuf.length);
        }

    }

    /**
     * Recursive call {@link #process(InputStream, OutputStream, boolean)} on
     * each file from "in" directory to "out" directory.
     *
     * @param in Directory to read files from
     * @param out Directory to write files to
     * @return copied files list
     */
    public List<String> processDirectory(File in, File out)
            throws FileNotFoundException, IOException, TemplateException {
        List<String> newFiles = new ArrayList<String>();
        if (in.isFile()) {
            if (out.isDirectory()) {
                out = new File(out, in.getName());
            }
            if (!out.getParentFile().exists()) {
                out.getParentFile().mkdirs();
            }

            boolean processAsText = false;
            boolean processAsFreemarker = false;
            String freemarkerExtension = null;
            if (!extensionsContainsDot) {
                int extIndex = in.getName().lastIndexOf('.');
                String extension = extIndex == -1 ? "" : in.getName().substring(extIndex + 1).toLowerCase();
                processAsText = plainTextExtensions == null || plainTextExtensions.contains(extension);
                if (freemarkerExtensions.contains(extension)) {
                    processAsFreemarker = true;
                    freemarkerExtension = extension;
                }

            } else {
                // Check for each extension if it matches end of filename
                String filename = in.getName().toLowerCase();
                for (String ext : plainTextExtensions) {
                    if (filename.endsWith(ext)) {
                        processAsText = true;
                        processAsFreemarker = false;
                        freemarkerExtension = ext;
                        break;
                    }
                }
                for (String ext : freemarkerExtensions) {
                    if (filename.endsWith(ext)) {
                        processAsText = false;
                        processAsFreemarker = true;
                        break;
                    }
                }
            }

            if (processAsFreemarker) {
                out = new File(
                        out.getCanonicalPath().replaceAll("\\.*" + Pattern.quote(freemarkerExtension) + "$", ""));
            }
            // Backup existing file if not already done
            if (out.exists()) {
                File backup = new File(out.getPath() + ".bak");
                if (!backup.exists()) {
                    log.debug("Backup " + out);
                    FileUtils.copyFile(out, backup);
                    newFiles.add(backup.getPath());
                }
            } else {
                newFiles.add(out.getPath());
            }
            if (processAsFreemarker) {
                log.debug("Process as FreeMarker " + in.getPath());
                processFreemarker(in, out);
            } else if (processAsText) {
                log.debug("Process as Text " + in.getPath());
                InputStream is = null;
                OutputStream os = null;
                try {
                    is = new FileInputStream(in);
                    os = new FileOutputStream(out);
                    processText(is, os);
                } finally {
                    IOUtils.closeQuietly(is);
                    IOUtils.closeQuietly(os);
                }
            } else {
                log.debug("Process as copy " + in.getPath());
                FileUtils.copyFile(in, out);
            }
        } else if (in.isDirectory()) {
            if (!out.exists()) {
                // allow renaming destination directory
                out.mkdirs();
            } else if (!out.getName().equals(in.getName())) {
                // allow copy over existing arborescence
                out = new File(out, in.getName());
                out.mkdir();
            }
            for (File file : in.listFiles()) {
                newFiles.addAll(processDirectory(file, out));
            }
        }
        return newFiles;
    }

    /**
     * @param extensionsList comma-separated list of files extensions to parse
     */
    public void setTextParsingExtensions(String extensionsList) {
        StringTokenizer st = new StringTokenizer(extensionsList, ",");
        plainTextExtensions = new ArrayList<String>();
        while (st.hasMoreTokens()) {
            String extension = st.nextToken();
            plainTextExtensions.add(extension);
            if (!extensionsContainsDot && extension.contains(".")) {
                extensionsContainsDot = true;
            }
        }
    }

    public void setFreemarkerParsingExtensions(String extensionsList) {
        StringTokenizer st = new StringTokenizer(extensionsList, ",");
        freemarkerExtensions = new ArrayList<String>();
        while (st.hasMoreTokens()) {
            String extension = st.nextToken();
            freemarkerExtensions.add(extension);
            if (!extensionsContainsDot && extension.contains(".")) {
                extensionsContainsDot = true;
            }
        }
    }

}