001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.utils;
019
020import io.stallion.exceptions.UsageException;
021import io.stallion.reflection.PropertyUtils;
022import io.stallion.services.Log;
023
024import java.util.HashMap;
025import java.util.Map;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import static io.stallion.utils.Literals.*;
030
031
032/**
033 * SimpleTemplating excepts a String with context tokens in the form of {object.attribute.attribute}
034 * You can populate the template with context objects.
035 * Then you render the template, and all tokens get replaced with the attribute values.
036 *
037 * For example:
038 *
039 * String result = new SimpleTemplate("My name is {user.displayName}")
040 *     .put("user", new User().setDisplayName("Peter Pan")
041 *     .render();
042 *
043 * Result will equal: "My name is Peter Pan"
044 *
045 *
046 */
047public class SimpleTemplate {
048    private static Pattern curlyPattern = Pattern.compile("\\{\\s*([\\w\\.]+)\\s*\\}");
049    private static Pattern doubleCurlyPattern = Pattern.compile("\\{\\{\\s*([\\w\\.]+)\\s*\\}\\}");
050    private static Pattern dollarPattern = Pattern.compile("\\$\\$\\s*([\\w\\.]+)\\s*\\$\\$");
051    private Map<String, Object> context;
052    private String template;
053    private Boolean strict = false;
054    private MatchPattern pattern = MatchPattern.DOLLAR.SINGLE_CURLY;
055
056    public static enum MatchPattern {
057        DOUBLE_CURLY,
058        SINGLE_CURLY,
059        DOLLAR
060    }
061
062    public SimpleTemplate(String template) {
063        this.template = template;
064        this.context = new HashMap<>();
065    }
066
067
068    public SimpleTemplate(String template, Map<String, Object> context) {
069        this(template, context, null);
070    }
071
072    public SimpleTemplate(String template, Map<String, Object> context, MatchPattern pattern) {
073        if (context == null) {
074            context = new HashMap<>();
075        }
076        if (pattern != null) {
077            this.pattern = pattern;
078        }
079        this.context = context;
080        this.template = template;
081    }
082
083    public SimpleTemplate putAll(Map<String, Object> context) {
084        this.context.putAll(context);
085        return this;
086    }
087
088    public SimpleTemplate put(String key, Object val) {
089        this.context.put(key, val);
090        return this;
091    }
092
093    public String render() {
094        Matcher matcher;
095        if (pattern.equals(MatchPattern.DOLLAR)) {
096            matcher = dollarPattern.matcher(template);
097        }  else if (pattern.equals(MatchPattern.DOUBLE_CURLY)) {
098            matcher = doubleCurlyPattern.matcher(template);
099        } else {
100            matcher = curlyPattern.matcher(template);
101        }
102        StringBuffer result = new StringBuffer();
103        for(Object c: safeLoop(1000)) {
104            if (!matcher.find()) {
105                break;
106            }
107            String replacement = getReplacement(matcher);
108            matcher.appendReplacement(result, replacement);
109        }
110        matcher.appendTail(result);
111        return result.toString();
112    }
113
114    private String getReplacement(Matcher matcher) {
115        String[] parts = matcher.group(1).split("\\.");
116        StringBuilder builder = new StringBuilder();
117        Object thing = context;
118        for (String part: parts) {
119            Object oldThing = thing;
120            if (thing instanceof Map) {
121                thing = ((Map)thing).get(part);
122            } else {
123                thing = PropertyUtils.getProperty(thing, part);
124            }
125            if (thing == null) {
126                String msg = "In token " + matcher.group(1) + " attribute " + part + " is null.";
127                if (strict) {
128                    throw new UsageException(msg);
129                } else {
130                    Log.warn(msg);
131                    return "";
132                }
133            }
134        }
135        return thing.toString();
136    }
137
138    public Boolean getStrict() {
139        return strict;
140    }
141
142    public void setStrict(Boolean strict) {
143        this.strict = strict;
144    }
145}