/* ================================================================== * StringMerger.java - Jan 14, 2010 9:13:00 AM * * Copyright 2007-2010 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== * $Id$ * ================================================================== */ package net.solarnetwork.util; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Map; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.beanutils.PropertyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.util.StringUtils; /** * Utility class for performing a simple mail-merge. * * <p> * This class parses a String source and substitutes variables in the form of * <code>${<b>name</b>}</code> for a corresponding property in a specified data * object. The data object can be either a <code>java.util.Map</code> or an * arbitrary JavaBean. If the data object is a Map, the variable names will be * treated as keys in that map, the the corresponding value will be substituted * in the output String. Otherwise, reflection will be used to access JavaBean * getter methods, using the Struts naming conventions for bean property names * and nested names. * </p> * * @author matt.magoffin * @version $Revision$ $Date$ */ public final class StringMerger { private static final Logger LOG = LoggerFactory.getLogger(StringMerger.class); private static final Pattern MERGE_VAR_PAT = Pattern.compile("\\$\\{([^}]+)\\}"); private StringMerger() { // do not instantiate } /** * Merge from a Resource, and return the merged output as a String. * * <p> * This method calls the {@link #mergeResource(Resource, Object, String)} * method, passing an empty string for <code>nullValue</code>. * </p> * * @param resource * the resource * @param data * the data to merge with * @return merged string * @throws IOException * if an error occurs */ public static String mergeResource(Resource resource, Object data) throws IOException { return mergeResource(resource, data, ""); } /** * Merge from a Resource, and return the merged output as a String. * * <p> * This method will read the Resource as character data line by line, * merging each line as it goes. * </p> * * @param resource * the resource * @param data * the data to merge with * @param nullValue * the value to substitute for null data elements * @return merged string * @throws IOException * if an error occurs */ public static String mergeResource(Resource resource, Object data, String nullValue) throws IOException { if ( LOG.isDebugEnabled() ) { LOG.debug("Merging " + resource.getFilename() + " with " + data); } InputStream in = null; try { in = resource.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder buf = new StringBuilder(); String oneLine = null; while ( (oneLine = reader.readLine()) != null ) { mergeString(oneLine, data, nullValue, buf); buf.append("\n"); } return buf.toString(); } finally { if ( in != null ) { try { in.close(); } catch ( IOException e ) { // ignore this } } } } /** * Merge from a file source, and return the merged output as a String. * * @return the merged output * @param filePath * path to the source file * @param data * the <code>Map</code> or JavaBean with merge data * @exception java.io.IOException * if an exception occurs */ public static String merge(String filePath, Object data) throws java.io.IOException { if ( LOG.isDebugEnabled() ) { LOG.debug("Merging " + filePath + " with " + data); } BufferedReader in = null; try { in = new BufferedReader(new FileReader(filePath)); StringBuilder buf = new StringBuilder(); String oneLine = null; while ( (oneLine = in.readLine()) != null ) { mergeString(oneLine, data, "", buf); buf.append("\n"); } return buf.toString(); } finally { if ( in != null ) { in.close(); } } } /** * Merge from a String source and return the result. * * @return java.lang.String * @param src * java.lang.String * @param nullValue * the value to substitute for null data * @param data * java.lang.Object */ public static String mergeString(String src, String nullValue, Object data) { StringBuilder buf = new StringBuilder(); mergeString(src, data, nullValue, buf); return buf.toString(); } /** * Merge from a String source into a StringBuilder. * * @param src * the source String to substitute into * @param data * the data object to substitute with * @param nullValue * the value to substitute for null data * @param buf * the StringBuilder to append the output to */ public static void mergeString(String src, Object data, String nullValue, StringBuilder buf) { Matcher matcher = MERGE_VAR_PAT.matcher(src); //MatchResult[] matches = MERGE_VAR_PAT.matcher(src); //REMatch[] matches = MERGE_VAR_RE.getAllMatches(src); if ( !matcher.find() ) { buf.append(src); } else { int endLastMatchIdx = 0; do { MatchResult matchResult = matcher.toMatchResult(); // append everything from the end of the last // match to the start of this match buf.append(src.substring(endLastMatchIdx, matchResult.start())); // perform substitution here... if ( data != null ) { int s = matchResult.start(1); int e = matchResult.end(1); if ( (s > -1) && (e > -1) ) { String varName = src.substring(s, e); if ( data instanceof java.util.Map<?, ?> ) { Object o = null; int sepIdx = varName.indexOf('.'); if ( sepIdx > 0 ) { String varName2 = varName.substring(sepIdx + 1); varName = varName.substring(0, sepIdx); o = ((Map<?, ?>) data).get(varName); if ( o != null ) { try { o = PropertyUtils.getProperty(o, varName2); } catch ( Exception e2 ) { LOG.warn("Exception getting property '" + varName2 + "' out of " + o.getClass() + ": " + e2); } } } else { // simply check for key o = ((Map<?, ?>) data).get(varName); } if ( o == null || (String.class.isAssignableFrom(o.getClass()) && !StringUtils .hasText(o.toString())) ) { buf.append(nullValue); } else { buf.append(o); } } else { // use reflection to get a bean property try { Object o = PropertyUtils.getProperty(data, varName); if ( o == null || (String.class.isAssignableFrom(o.getClass()) && !StringUtils .hasText(o.toString())) ) { buf.append(nullValue); } else { buf.append(o); } } catch ( Exception ex ) { LOG.warn("Exception getting property '" + varName + "' out of " + data.getClass() + ": " + ex); buf.append(nullValue); } } } endLastMatchIdx = matchResult.end(); } } while ( matcher.find() ); if ( endLastMatchIdx < src.length() ) { buf.append(src.substring(endLastMatchIdx)); } } } }