views:

594

answers:

2

I have a service method that takes a String and then replaces tags in the String with items from a tag library. As follows:

for( MetaDataDTO tag : tagValues )
{
    message = message.replace( tag.getKey(), tag.getText1() );
}

Obviously; this make heaps of new strings and is BAD. But the StringBuilder replace method is cumbersome to use for multiple strings inside one string. How can I make my method more efficient?

It is for use with blocks of text such as:

Dear #firstName#, your application for #applicationType# has been #approvedRejected# sorry.

Where #firstName#, etc are the keys in a meta data database. It is also possible that tags may not be surrounded by hash characters.

+5  A: 

Basically you want to copy the execution of Matcher.replaceAll() like so:

public static String replaceTags(String message, Map<String, String> tags) {
  Pattern p = Pattern.compile("#(\\w+)#");
  Matcher m = p.matcher(message);
  boolean result = m.find();
  if (result) {
    StringBuffer sb = new StringBuffer();
    do {
      m.appendReplacement(sb, tags.containsKey(m.group(1) ? tags.get(m.group(1)) : "");
      result = m.find();
    } while (result);
    m.appendTail(sb);
    message = sb.toString();
  }
  return message;
}

Note: I've made an assumption about the valid tag (namely \w in the regex). You will need to cater this for what's really valid (eg "#([\w_]+)#").

I've also assumed the tags above looks something like:

Map<String, String> tags = new HashMap<String, String>();
tags.add("firstName", "Skippy");

and not:

tags.add("#firstName#", "Skippy");

If the second is correct you'll need to adjust accordingly.

This method makes exactly one pass across the message string so it doesn't get much more efficient than this.

cletus
should that be m.appendReplacement?
Martlark
Yes, fixed.Thanks.
cletus
One simple efficiency improvement you could make is to compile the regular expression outside of the method call: private static Pattern _tagPattern = Pattern.compile(...); public static String replaceTags(...) { Matcher m = _tagPattern.matcher(message); }
Henry
@Henry: I used to do things like that but my testing on modern JVMs indicates the cost of creating the pattern to be close to nothing. Also, if you do that you then introduce concurrency/locking issues. A much simpler and less error-prone design is worth the slightly higher cost.
cletus
@cletus just regarding the concurrency concern, the javadoc for the Pattern class says "instances of this class are immutable and are safe for use by multiple concurrent threads".
Henry
A: 

Thanks for your help guys. Certainly learned more about java. Here is my solution. It is this way to support different looking tags and tags within tags:

private static String replaceAllTags(String message, Map< String, String > tags)
{
 StringBuilder sb = new StringBuilder( message );
 boolean tagFound = false;
 /**
  * prevent endless circular text replacement loops
  */
 long recurrancyChecker = 5000;

 do
 {
  tagFound = false;
  Iterator it = tags.entrySet().iterator();
  while( it.hasNext() )
  {
   Map.Entry pairs = (Map.Entry) it.next();

   int start = sb.indexOf( pairs.getKey().toString() );

   while( start > -1 && --recurrancyChecker > 0 )
   {
    int length = pairs.getKey().toString().length();
    sb.replace( start, start + length, pairs.getValue().toString() );
    start = sb.indexOf( pairs.getKey().toString() );
    tagFound = true;
   }
  }
 }
 while( tagFound && --recurrancyChecker > 0 );
 return sb.toString();
}
Martlark