package org.webpieces.templating.impl.tags; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.beanutils.PropertyUtils; import org.webpieces.ctx.api.Current; import org.webpieces.ctx.api.Flash; import org.webpieces.ctx.api.Validation; import org.webpieces.templating.api.ClosureUtil; import org.webpieces.templating.api.ConverterLookup; import org.webpieces.templating.api.HtmlTag; import org.webpieces.templating.impl.GroovyTemplateSuperclass; import org.webpieces.util.logging.Logger; import org.webpieces.util.logging.LoggerFactory; import groovy.lang.Closure; /** * challenging to get right so heavily tested * * 1. on first GET request must render bean from an enum, array, collection, or just prmitive field as string * 2. on second GET request must render flash IF set(even if null!!!) or the bean BUT the twist is flash is all Strings * * * @author dhiller * */ public class FieldTag extends TemplateLoaderTag implements HtmlTag { private static final Logger log = LoggerFactory.getLogger(FieldTag.class); private Pattern pattern = Pattern.compile("\\[(.*?)\\]"); //for arrays only private String fieldHtmlPath; private ConverterLookup converter; public FieldTag(ConverterLookup converter, String fieldHtmlPath) { this.converter = converter; this.fieldHtmlPath = fieldHtmlPath; } @Override public String getName() { return "field"; } protected String getErrorClass() { return "error"; } @Override protected String getFilePath(GroovyTemplateSuperclass callingTemplate, Map<Object, Object> args, String srcLocation) { return fieldHtmlPath; } @Override protected Map<String, Object> convertTagArgs(Map<Object, Object> tagArgs, Map<String, Object> pageArgs, Closure<?> body, String srcLocation) { if(tagArgs.get("_body") != null) throw new IllegalArgumentException("tag "+getName()+" must not define an argument of '_body' as that is reserved and will be overwritten"+srcLocation); else if(tagArgs.get("field") != null) throw new IllegalArgumentException("tag "+getName()+" must not define an argument of 'field' as that is reserved and will be overwritten "+srcLocation); String fieldName = tagArgs.get("defaultArgument").toString(); Map<String, Object> field = createFieldData(fieldName, pageArgs); Map<String, Object> copyOfTagArgs = new HashMap<>(); Map<String, Object> closureProps = new HashMap<>(); for(Map.Entry<Object, Object> entry : tagArgs.entrySet()) { String key = entry.getKey().toString(); copyOfTagArgs.put(key, entry.getValue()); body.setProperty(key, entry.getValue()); closureProps.put(key, entry.getValue()); } copyOfTagArgs.put("field", field); String bodyStr = ""; if(body != null) { body.setProperty("field", field); closureProps.put("field", field); bodyStr = ClosureUtil.toString(getName(), body, closureProps); } //variables starting with _ will not be html escaped so the body html won't be converted like other variables copyOfTagArgs.put("_body", bodyStr); return copyOfTagArgs; } /** * */ @SuppressWarnings("rawtypes") private Map<String, Object> createFieldData(String fieldName2, Map<String, Object> pageArgs) { Result result = reworkNameForArrayOnly(fieldName2, pageArgs); String fieldName = result.fieldName; Flash flash = Current.flash(); Validation validation = Current.validation(); Map<String, Object> field = new HashMap<String, Object>(); field.put("name", fieldName); String id = makeValidHtml4Id(fieldName); field.put("id", id); String flashValue = flash.get(fieldName); field.put("i18nKey", result.i18nName); //different from fieldName only for Arrays field.put("flash", flashValue); field.put("error", validation.getError(fieldName)); field.put("errorClass", field.get("error") != null ? getErrorClass() : ""); String[] pieces = fieldName.split("\\."); Object pageArgValue = null; Object obj = pageArgs.get(pieces[0]); if (pieces.length > 1) { try { String path = fieldName.substring(fieldName.indexOf(".") + 1); pageArgValue = PropertyUtils.getProperty(obj, path); } catch (Exception e) { // if there is a problem reading the field we dont set any // value log.trace(() -> "exception", e); } } else { //for method parameters not fields in the bean like above pageArgValue = obj; } field.put("value", pageArgValue); String valAsStr = null; if(pageArgValue instanceof Collection) { //For multiple select html OptionTag to work, this was needed valAsStr = convert((Collection)pageArgValue); } else { valAsStr = converter.convert(pageArgValue); } field.put("flashOrValue", preferFirst(flashValue, valAsStr)); field.put("valueOrFlash", preferFirst(valAsStr, flashValue)); return field; } @SuppressWarnings("rawtypes") private String convert(Collection pageArgValue) { Iterator iterator = pageArgValue.iterator(); String result = ""; String seperator = ""; while(iterator.hasNext()) { Object bean = iterator.next(); String valAsStr = converter.convert(bean); result += seperator + valAsStr; if(seperator.equals("")) seperator = ","; } return result; } protected Result reworkNameForArrayOnly(String fieldName, Map<String, Object> pageArgs) { if(!fieldName.contains("[")) return new Result(fieldName, fieldName); //modify field name to be array format with real index //go from user.accounts[account_index].addresses[address_index].street to // user.accounts[0].addresses[1].street such that PropertyUtils.getProperty(bean, fieldName) works //i18n name will be 'user.accounts.addresses.street'; String i18nName = fieldName; Matcher m = pattern.matcher(fieldName); while(m.find()) { String indexName = m.group(1); Object index = pageArgs.get(indexName); fieldName = fieldName.replace(indexName, index+""); i18nName = i18nName.replace("["+indexName+"]", ""); } return new Result(fieldName, i18nName); } private String makeValidHtml4Id(String fieldName) { return fieldName.replace('.', '_').replace("[", ":").replace("]", ":"); } private String preferFirst(String first, String last) { if(first != null) return first; return last; } private static class Result { public String fieldName; public String i18nName; public Result(String fieldName, String i18nName) { this.fieldName = fieldName; this.i18nName = i18nName; } } }