/* Milyn - Copyright (C) 2006 This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (version 2.1) as published by the Free Software Foundation. This library 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 Lesser General Public License for more details: http://www.gnu.org/licenses/lgpl.txt */ package org.milyn.javabean; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.*; import org.milyn.cdr.*; import org.milyn.cdr.annotation.*; import org.milyn.container.ApplicationContext; import org.milyn.delivery.*; import org.milyn.delivery.annotation.*; import org.milyn.delivery.dom.*; import org.milyn.javabean.ext.*; import org.milyn.xml.*; import org.w3c.dom.*; import java.io.*; import java.util.*; /** * Javabean Populator. * <h2>Sample Configuration</h2> * Populate an <b><code>Order</code></b> bean with <b><code>Header</code></b> and <b><code>OrderItem</code></b> * beans (OrderItems added to a list). * * <h4>Input XML</h4> * <pre> * <order> * <header> * <date>Wed Nov 15 13:45:28 EST 2006</date> * <customer number="123123">Joe</customer> * </header> * <order-items> * <order-item> * <product>111</product> * <quantity>2</quantity> * <price>8.90</price> * </order-item> * <order-item> * <product>222</product> * <quantity>7</quantity> * <price>5.20</price> * </order-item> * </order-items> * </order> * </pre> * <h4 id="targetbeans">Target Java Beans</h4> * (Not including getters and setters): * <pre> * public class Order { * private Header header; * private List<OrderItem> orderItems; * } * public class Header { * private Date date; * private Long customerNumber; * private String customerName; * } * public class OrderItem { * private long productId; * private Integer quantity; * private double price; * } * </pre> * <h4>Smooks Configuration</h4> * The following configuration, when applied to the input XML, will create (and populate) the object graph * defined by the <a href="#targetbeans">Target Java Beans</a> using the data in the input XML. * <pre> * <?xml version="1.0"?> * <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.0.xsd"> * * <-- Create the Order bean instance when we encounter the "order" element * and call it "order"... --> * <resource-config selector="order"> * <resource>org.milyn.javabean.BeanPopulator</resource> * <param name="beanId"><b><u>order</u></b></param> * <param name="beanClass"><b>org.milyn.javabean.Order</b></param> * <param name="bindings"> * <binding property="header" selector="${header}" /> <-- Wire the header bean to the header property. See header configuration below... --> * <binding property="orderItems" selector="${orderItems}" /> <-- Wire the orderItems ArrayList to the orderItems property. See orderItems configuration below... --> * </param> * </resource-config> * * <-- Create a List for the OrderItem instances when we encounter the "order" element. * Call it "orderItems" and set it on the "order" bean... --> * <resource-config selector="order"> * <resource>org.milyn.javabean.BeanPopulator</resource> * <param name="beanId"><b><u>orderItems</u></b></param> * <param name="beanClass"><b>{@link ArrayList java.util.ArrayList}</b></param> * <param name="bindings"> * <binding selector="${orderItem}" /> <-- Wire the orderItem to this ArrayList. See order-item configuration below... --> * </param> * </resource-config> * * <-- Create the Header bean instance when we encounter the "header" element. * Call it "header" --> * <resource-config selector="header"> * <resource>org.milyn.javabean.BeanPopulator</resource> * <param name="beanId"><b><u>header</u></b></param> * <param name="beanClass"><b>org.milyn.javabean.Header</b></param> * <param name="bindings"> * <-- Header bindings... --> * <binding property="date" type="OrderDateLong" selector="header/date" /> <-- See OrderDateLong decoder definition below... --> * <binding property="customerNumber" type="{@link org.milyn.javabean.decoders.LongDecoder Long}" selector="header/customer/@number" /> * <binding property="customerName" selector="header/customer" /> <-- Type defaults to String --> * </param> * </resource-config> * * <-- Create OrderItem instances when we encounter the "order-item" element. * Set them on the "orderItems" bean (List)... --> * <resource-config selector="order-item"> * <resource>org.milyn.javabean.BeanPopulator</resource> * <param name="beanClass"><b>org.milyn.javabean.OrderItem</b></param> * <param name="bindings"> * <-- OrderItem bindings... --> * <binding property="productId" type="{@link org.milyn.javabean.decoders.LongDecoder Long}" selector="order-item/product" /> * <binding property="quantity" type="{@link org.milyn.javabean.decoders.IntegerDecoder Integer}" selector="order-item/quantity" /> * <binding property="price" type="{@link org.milyn.javabean.decoders.DoubleDecoder Double}" selector="order-item/price" /> * </param> * </resource-config> * * <-- Explicitly defining a decoder for the date field on the order header so as to define the format... --> * <resource-config selector="decoder:OrderDateLong"> * <resource>{@link org.milyn.javabean.decoders.DateDecoder org.milyn.javabean.decoders.DateDecoder}</resource> * <param name="format">EEE MMM dd HH:mm:ss z yyyy</param> * </resource-config> * * </smooks-resource-list> * </pre> * * <h4>Smooks Configuration</h4> * To trigger this visitor during the Assembly Phase, simply set the "VisitPhase" param to "ASSEMBLY". * * @author tfennelly * @author <a href="mailto:maurice.zeijen@smies.com">maurice.zeijen@smies.com</a> * @deprecated Use the XSD based configuration. */ public class BeanPopulator implements ConfigurationExpander { private static Log logger = LogFactory.getLog(BeanPopulator.class); public static String GLOBAL_DEFAULT_EXTEND_LIFECYCLE = "binding.extend.lifecycle"; @ConfigParam(name="beanId", defaultVal = AnnotationConstants.NULL_STRING) private String beanIdName; @ConfigParam(name="beanClass", defaultVal = AnnotationConstants.NULL_STRING) private String beanClassName; @ConfigParam(defaultVal = "true") private boolean create; @ConfigParam(defaultVal = AnnotationConstants.NULL_STRING) private String extendLifecycle; @Config private SmooksResourceConfiguration config; @AppContext private ApplicationContext appContext; /******************************************************************************************************* * Common Methods. *******************************************************************************************************/ /** * Set the resource configuration on the bean populator. * @throws SmooksConfigurationException Incorrectly configured resource. */ @Initialize public void initialize() throws SmooksConfigurationException { // One of "beanId" or "beanClass" must be specified... if (beanClassName == null || StringUtils.isBlank(beanClassName)) { throw new SmooksConfigurationException("Invalid Smooks bean configuration. 'beanClass' <param> not specified."); } beanClassName = beanClassName.trim(); // May need to default the "beanId"... if (beanIdName == null || beanIdName.trim().length() == 0) { beanIdName = toBeanId(beanClassName); logger.debug("No 'beanId' specified for beanClass '" + beanClassName + "'. Defaulting beanId to '" + beanIdName + "'."); } if (config.getStringParameter("attributeName") != null) { throw new SmooksConfigurationException("Invalid Smooks bean configuration. 'attributeName' param config no longer supported. Please use the <bindings> config style."); } if (config.getStringParameter("setterName") != null) { throw new SmooksConfigurationException("Invalid Smooks bean configuration. 'setterName' param config no longer supported. Please use the <bindings> config style."); } logger.debug("Bean Populator created for [" + beanIdName + ":" + beanClassName + "]."); } public List<SmooksResourceConfiguration> expandConfigurations() throws SmooksConfigurationException { List<SmooksResourceConfiguration> resources = new ArrayList<SmooksResourceConfiguration>(); buildInstanceCreatorConfig(resources); buildBindingConfigs(resources); return resources; } private void buildInstanceCreatorConfig(List<SmooksResourceConfiguration> resources) { SmooksResourceConfiguration resource = (SmooksResourceConfiguration) config.clone(); // Reset the beanId and beanClass parameters resource.removeParameter("beanId"); resource.setParameter("beanId", beanIdName); resource.removeParameter("beanClass"); resource.setParameter("beanClass", beanClassName); // Remove the bindings param... resource.removeParameter("bindings"); if(!create) { resource.setSelector(SmooksResourceConfiguration.DOCUMENT_VOID_SELECTOR); } // Reset the resource... resource.setResource(BeanInstanceCreator.class.getName()); resources.add(resource); } private void buildBindingConfigs(List<SmooksResourceConfiguration> resources) { Parameter bindingsParam = config.getParameter("bindings"); if (bindingsParam != null) { Element bindingsParamElement = bindingsParam.getXml(); if(bindingsParamElement != null) { NodeList bindings = bindingsParamElement.getElementsByTagName("binding"); try { for (int i = 0; bindings != null && i < bindings.getLength(); i++) { Element node = (Element)bindings.item(i); resources.add(buildInstancePopulatorConfig(node)); } } catch (IOException e) { throw new SmooksConfigurationException("Failed to read binding configuration for " + config, e); } } else { logger.error("Sorry, the Javabean populator bindings must be available as XML DOM. Please configure using XML."); } } } private SmooksResourceConfiguration buildInstancePopulatorConfig(Element bindingConfig) throws IOException, SmooksConfigurationException { SmooksResourceConfiguration resourceConfig; String selector; String selectorNamespace; String property; String setterMethod; String type; String defaultVal; String wireBeanId = null; // Make sure there's both 'selector' and 'property' attributes... selector = getSelectorAttr(bindingConfig); //Check if we get a bean wiring, if so then we need to change the selector to selector of current config so that the //BeanInstanceCreator is called on that node instead of one off the child nodes. //The wireBeanId indicates the beanId that should be selected if(selector.startsWith("${") && selector.endsWith("}")) { wireBeanId = selector.substring(2, selector.length() - 1); selector = config.getSelector(); } setterMethod = DomUtils.getAttributeValue(bindingConfig, "setterMethod"); property = DomUtils.getAttributeValue(bindingConfig, "property"); // Extract the binding config properties from the selector and property values... String[] selectorTokens = SmooksResourceConfiguration.parseSelector(selector); String attributeNameProperty = SmooksResourceConfiguration.extractTargetAttribute(selectorTokens); String selectorProperty = SelectorPropertyResolver.getSelectorProperty(selectorTokens); // Construct the configuraton... resourceConfig = new SmooksResourceConfiguration(selectorProperty, BeanInstancePopulator.class.getName()); resourceConfig.setParameter(VisitPhase.class.getSimpleName(), config.getStringParameter(VisitPhase.class.getSimpleName(), VisitPhase.PROCESSING.toString())); resourceConfig.setParameter("beanId", beanIdName); if(wireBeanId != null) { resourceConfig.setParameter("wireBeanId", wireBeanId); } if(setterMethod != null) { resourceConfig.setParameter("setterMethod", setterMethod); } if(property != null) { resourceConfig.setParameter("property", property); } if (attributeNameProperty != null && !attributeNameProperty.trim().equals("")) { // The value is comming out of an attribute on the target element. // If this attribute is not defined, the value will be taken from the element text... resourceConfig.setParameter("valueAttributeName", attributeNameProperty); } else if(wireBeanId == null) { // It's not a bean wiring binding and it's not an attribute value binding. Check // was there a nested expression in the binding. This expression can be used // to extract the population value... String expression = DomUtils.getAllText(bindingConfig, true); if(expression != null) { expression = expression.trim(); if(!expression.equals("")) { resourceConfig.setParameter("expression", expression); } } } type = DomUtils.getAttributeValue(bindingConfig, "type"); defaultVal = DomUtils.getAttributeValue(bindingConfig, "default"); if(wireBeanId == null ) { // Set the data type... if(type != null) { resourceConfig.setParameter("type", type); } if(defaultVal != null) { resourceConfig.setParameter("default", defaultVal); } } else { if(type != null) { throw new SmooksConfigurationException("The 'type' attribute isn't a allowed when binding a bean: " + bindingConfig); } if(defaultVal != null) { throw new SmooksConfigurationException("The 'default' attribute isn't a allowed when binding a bean: " + bindingConfig); } } resourceConfig.setTargetProfile(config.getTargetProfile()); // Set the selector namespace... selectorNamespace = DomUtils.getAttributeValue(bindingConfig, "selector-namespace"); if(selectorNamespace == null) { selectorNamespace = config.getSelectorNamespaceURI(); } resourceConfig.setSelectorNamespaceURI(selectorNamespace); if (extendLifecycle != null) { resourceConfig.setParameter("extendLifecycle", extendLifecycle); } return resourceConfig; } private String toBeanId(String beanClassName) { String[] beanClassNameTokens = beanClassName.split("\\."); StringBuffer simpleClassName = new StringBuffer(beanClassNameTokens[beanClassNameTokens.length - 1]); // Lowercase the first char... simpleClassName.setCharAt(0, Character.toLowerCase(simpleClassName.charAt(0))); return simpleClassName.toString(); } private String getSelectorAttr(Element bindingConfig) { String selector = DomUtils.getAttributeValue(bindingConfig, "selector"); if (selector == null) { selector = config.getSelector(); } return selector; } }