/*
* Copyright (C) NetStruxr, Inc. All rights reserved.
*
* This software is published under the terms of the NetStruxr
* Public Software License version 0.5, a copy of which has been
* included with this distribution in the LICENSE.NPL file. */
package er.extensions.foundation;
import java.util.Enumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOApplication;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSMutableSet;
import er.extensions.logging.ERXPatternLayout;
/**
* Very simple template parser. For example if you have the delimiter:
* {@literal @}{@literal @}, then a possible template might look like: "Hello, {@literal @}{@literal @}name{@literal @}{@literal @}. How are
* you feeling today?", In this case the object will get asked for the
* value name. This works with key-paths as well.
*/
public class ERXSimpleTemplateParser {
/** The default label for keys not found while parsing */
public static final String DEFAULT_UNDEFINED_KEY_LABEL = "?";
/** The default delimiter */
public static final String DEFAULT_DELIMITER = "@@";
/** logging support */
private static final Logger log = LoggerFactory.getLogger(ERXSimpleTemplateParser.class);
/** holds a reference to the shared instance of the parser */
private static ERXSimpleTemplateParser _sharedInstance;
/**
* Convenience method to return the shared instance
* of the template parser.
*
* @return shared instance of the parser
* @see #setSharedInstance
*/
public static ERXSimpleTemplateParser sharedInstance() {
if (_sharedInstance == null)
setSharedInstance(new ERXSimpleTemplateParser());
return _sharedInstance;
}
/**
* Sets the shared instance of the template parser.
*
* @param newSharedInstance the parser object that will be shared
* @see #sharedInstance
*/
public static synchronized void setSharedInstance(ERXSimpleTemplateParser newSharedInstance) {
_sharedInstance = newSharedInstance;
}
/**
* Flag to disable logging. {@link ERXPatternLayout} will set
* this to true for its internal parser object in order to
* prevent an infinite debug logging loop.
*/
public boolean isLoggingDisabled = false;
/** The label that will be appeared where an undefined key is found */
private final String _undefinedKeyLabel;
/**
* Returns a parser object with the default undefined label
*
* @see #DEFAULT_UNDEFINED_KEY_LABEL
*/
public ERXSimpleTemplateParser() {
this(DEFAULT_UNDEFINED_KEY_LABEL);
}
/**
* Returns a parser object with the given string as the undefined key label
*
* @param undefinedKeyLabel string as the undefined key label,
* for example, "?", "N/A"
*/
public ERXSimpleTemplateParser(String undefinedKeyLabel) {
super();
_undefinedKeyLabel = (undefinedKeyLabel == null ? DEFAULT_UNDEFINED_KEY_LABEL : undefinedKeyLabel);
}
/**
* Calculates the set of keys used in a given template
* for a given delimiter.
*
* @param template to check for keys
* @param delimiter for finding keys
* @return array of keys
*/
public NSArray keysInTemplate(String template, String delimiter) {
NSMutableSet keys = new NSMutableSet();
if (delimiter == null) {
delimiter = DEFAULT_DELIMITER;
}
NSArray components = NSArray.componentsSeparatedByString(template, delimiter);
if (! isLoggingDisabled) {
log.debug("Components: {}", components);
}
boolean deriveElement = false; // if the template starts with delim, the first component will be a zero-length string
for (Enumeration e = components.objectEnumerator(); e.hasMoreElements();) {
String element = (String)e.nextElement();
if (deriveElement) {
if (element.length() == 0) {
throw new IllegalArgumentException("\"\" is not a valid keypath");
}
keys.addObject(element);
deriveElement = false;
} else {
deriveElement = true;
}
}
return keys.allObjects();
}
/**
* Cover method for calling the four argument method
* passing in <code>null</code> for the <code>otherObject</code>
* parameter. See that method for documentation.
*
* @param template to use to parse
* @param delimiter to use to find keys
* @param object to resolve keys
* @return parsed template with keys replaced
*/
public String parseTemplateWithObject(String template, String delimiter, Object object) {
return parseTemplateWithObject(template,
delimiter,
object,
null);
}
/**
* This method replaces the keys enclosed between the
* delimiter with the values found in object and otherObject.
* It first looks for a value in object, and then in otherObject
* if the key is not found in object. Therefore, otherObject is
* a good place to store default values while object is a
* good place to override default values.
* <p>
* When the value is not found in both object and otherObject,
* it will replace the key with the undefined key label which
* defaults to "?". You can set the label via the constructor
* {@link #ERXSimpleTemplateParser(String)}. Note that a <code>null</code>
* result will also output the label, so you might want to have the empty
* string as the undefined key label.
*
* @param template to use to parse
* @param delimiter to use to check for keys
* @param object to resolve keys off of
* @param otherObject object used to resolve default keys
* @return parsed template with keys replaced
*/
public String parseTemplateWithObject(String template, String delimiter, Object object, Object otherObject) {
if (template == null)
throw new IllegalArgumentException("Attempting to parse null template!");
if (object == null) {
throw new IllegalArgumentException("Attempting to parse template with null object!");
}
if (delimiter == null) {
delimiter = DEFAULT_DELIMITER;
}
if (! isLoggingDisabled) {
log.debug("Parsing template: {} with delimiter: {} object: {}",template, delimiter, object);
log.debug("Template: {}", template);
log.debug("Delim: {}", delimiter);
log.debug("otherObject: {}", otherObject);
}
NSArray components = NSArray.componentsSeparatedByString(template, delimiter);
if (! isLoggingDisabled) {
log.debug("Components: {}", components);
}
boolean deriveElement = false; // if the template starts with delim, the first component will be a zero-length string
StringBuilder sb = new StringBuilder();
Object objects[];
if (otherObject != null) {
objects = new Object[] {object, otherObject};
} else {
objects = new Object[] {object};
}
for (Enumeration e = components.objectEnumerator(); e.hasMoreElements();) {
String element = (String)e.nextElement();
if(!isLoggingDisabled) {
log.debug("Processing Element: {}", element);
}
if(deriveElement) {
if(!isLoggingDisabled) {
log.debug("Deriving value ...");
}
if(element.length() == 0) {
throw new IllegalArgumentException("\"\" is not a valid keypath in template: " + template);
}
Object result = _undefinedKeyLabel;
for (int i = 0; i < objects.length; i++) {
Object o = objects[i];
if(o != null && result == _undefinedKeyLabel) {
try {
if(!isLoggingDisabled) {
log.debug("calling valueForKeyPath({}, {})", o, element);
}
result = doGetValue(element, o);
// For just in case the above doesn't throw an exception when the
// key is not defined. (NSDictionary doesn't seem to throw the exception.)
if(result == null) {
result = _undefinedKeyLabel;
}
} catch (NSKeyValueCoding.UnknownKeyException t) {
result = _undefinedKeyLabel;
} catch (Throwable t) {
throw new NSForwardException(t, "An exception occured while parsing element, "
+ element + ", of template, \""
+ template + "\": "
+ t.getMessage());
}
}
}
if(result == _undefinedKeyLabel) {
if (!isLoggingDisabled) {
log.debug("Could not find a value for '{}' of template, '{}' in either the object or extra data.", element, template);
}
}
sb.append(result.toString());
deriveElement = false;
} else {
if(element.length() > 0) {
sb.append(element);
}
deriveElement = true;
}
if(!isLoggingDisabled) {
log.debug("Buffer: {}", sb);
}
}
return sb.toString();
}
/**
* To allow flexibility of the variable provider object type we use similar
* logic to NSDictionary valueForKeyPath. Consequently
* <code>java.util.Properties</code> objects that use keyPath separator (.)
* in the property names (which is common) can be reliably used as object
* providers.
*
* @param aKeyPath
* @param anObject
* @return the value corresponding to either a key with value
* <code>aKeypath</code>, or when no key, a keyPath with value
* <code>aKeyPath</code>
*/
protected Object doGetValue(String aKeyPath, Object anObject) {
// Mimic NSDictionary valueForKeypath behavior which first checks for a
// "flattened" key before calling real valueForKeypath logic
Object result = null;
try {
result = NSKeyValueCoding.Utility.valueForKey(anObject, aKeyPath);
}
catch (NSKeyValueCoding.UnknownKeyException t) {
}
if (result == null) {
return NSKeyValueCodingAdditions.Utility.valueForKeyPath(anObject, aKeyPath);
}
return result;
}
/**
* Parses the given templateString with an ERXSimpleTemplateParser.
*
* @param templateString the template string to parse
* @param templateObject the object to bind to
* @return the parsed template string
*/
public static String parseTemplatedStringWithObject(String templateString, Object templateObject) {
String convertedValue = templateString;
if (templateString == null || templateString.indexOf(DEFAULT_DELIMITER) == -1) {
return templateString;
}
String lastConvertedValue = null;
while (convertedValue != lastConvertedValue && convertedValue.indexOf(DEFAULT_DELIMITER) > -1) {
lastConvertedValue = convertedValue;
convertedValue = new ERXSimpleTemplateParser("ERXSystem:KEY_NOT_FOUND").parseTemplateWithObject(convertedValue, DEFAULT_DELIMITER, templateObject, WOApplication.application());
}
if (convertedValue.indexOf("ERXSystem:KEY_NOT_FOUND") > -1) {
log.warn("Not all keys in templateString were present in templateObject, returning unmodified templateString.");
return templateString; // not all keys are present
}
return convertedValue;
}
}