org.smooks.cartridges.javabean.BeanInstancePopulator Maven / Gradle / Ivy
The newest version!
/*-
* ========================LICENSE_START=================================
* smooks-javabean-cartridge
* %%
* Copyright (C) 2020 Smooks
* %%
* Licensed under the terms of the Apache License Version 2.0, or
* the GNU Lesser General Public License version 3.0 or later.
*
* SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-or-later
*
* ======================================================================
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* ======================================================================
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* =========================LICENSE_END==================================
*/
package org.smooks.cartridges.javabean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smooks.api.ApplicationContext;
import org.smooks.api.ExecutionContext;
import org.smooks.api.SmooksConfigException;
import org.smooks.api.SmooksException;
import org.smooks.api.bean.context.BeanContext;
import org.smooks.api.bean.context.BeanIdStore;
import org.smooks.api.bean.lifecycle.BeanContextLifecycleEvent;
import org.smooks.api.bean.lifecycle.BeanLifecycle;
import org.smooks.api.bean.repository.BeanId;
import org.smooks.api.converter.TypeConverter;
import org.smooks.api.converter.TypeConverterDescriptor;
import org.smooks.api.converter.TypeConverterException;
import org.smooks.api.converter.TypeConverterFactory;
import org.smooks.api.delivery.ContentDeliveryConfig;
import org.smooks.api.delivery.fragment.Fragment;
import org.smooks.api.delivery.ordering.Consumer;
import org.smooks.api.delivery.ordering.Producer;
import org.smooks.api.resource.config.ResourceConfig;
import org.smooks.api.resource.visitor.VisitAfterReport;
import org.smooks.api.resource.visitor.VisitBeforeReport;
import org.smooks.api.resource.visitor.sax.ng.AfterVisitor;
import org.smooks.api.resource.visitor.sax.ng.BeforeVisitor;
import org.smooks.api.resource.visitor.sax.ng.ChildrenVisitor;
import org.smooks.cartridges.javabean.converter.PreprocessTypeConverterFactory;
import org.smooks.cartridges.javabean.observers.BeanWiringObserver;
import org.smooks.cartridges.javabean.observers.ListToArrayChangeObserver;
import org.smooks.engine.bean.lifecycle.DefaultBeanContextLifecycleEvent;
import org.smooks.engine.converter.StringConverterFactory;
import org.smooks.engine.delivery.fragment.NodeFragment;
import org.smooks.engine.expression.MVELExpressionEvaluator;
import org.smooks.engine.lookup.NamespaceManagerLookup;
import org.smooks.engine.lookup.converter.NameTypeConverterFactoryLookup;
import org.smooks.engine.lookup.converter.SourceTargetTypeConverterFactoryLookup;
import org.smooks.engine.memento.TextAccumulatorMemento;
import org.smooks.engine.memento.TextAccumulatorVisitorMemento;
import org.smooks.support.ClassUtils;
import org.smooks.support.DomUtils;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Element;
import jakarta.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Bean instance populator visitor class.
*
*
* @author [email protected]
* @author [email protected]
*/
@VisitBeforeReport(condition = "parameters.containsKey('wireBeanId') || parameters.containsKey('valueAttributeName')",
summary = "<#if resource.parameters.wireBeanId??>Create bean lifecycle observer for bean ${resource.parameters.wireBeanId}." +
"<#else>Populating ${resource.parameters.beanId} with the value from the attribute ${resource.parameters.valueAttributeName}.#if>",
detailTemplate = "reporting/BeanInstancePopulatorReport_Before.html")
@VisitAfterReport(condition = "!parameters.containsKey('wireBeanId') && !parameters.containsKey('valueAttributeName')",
summary = "Populating ${resource.parameters.beanId} with a value from this element.",
detailTemplate = "reporting/BeanInstancePopulatorReport_After.html")
public class BeanInstancePopulator implements BeforeVisitor, AfterVisitor, ChildrenVisitor, Producer, Consumer {
protected static final Logger LOGGER = LoggerFactory.getLogger(BeanInstancePopulator.class);
protected static final String EXPRESSION_VALUE_VARIABLE_NAME = "_VALUE";
public static final String VALUE_ATTRIBUTE_NAME = "valueAttributeName";
public static final String VALUE_ATTRIBUTE_PREFIX = "valueAttributePrefix";
public static final String NOTIFY_POPULATE = "org.smooks.cartridges.javabean.notify.populate";
protected String id;
@Inject
@Named("beanId")
protected String beanIdName;
@Inject
@Named("wireBeanId")
protected Optional wireBeanIdName;
@Inject
protected Optional> wireBeanType;
@Inject
protected Optional> wireBeanAnnotation;
@Inject
protected Optional expression;
protected MVELExpressionEvaluator expressionEvaluator;
protected boolean expressionHasDataVariable = false;
@Inject
protected Optional property;
@Inject
protected Optional setterMethod;
@Inject
protected Optional valueAttributeName;
@Inject
protected Optional valueAttributePrefix;
protected String valueAttributeNS;
@Inject
@Named("type")
protected Optional typeAlias;
@Inject
@Named("default")
protected Optional defaultVal;
@Inject
@Named(NOTIFY_POPULATE)
protected Boolean notifyPopulate = false;
@Inject
protected ResourceConfig config;
@Inject
protected ApplicationContext applicationContext;
protected BeanIdStore beanIdStore;
protected BeanId beanId;
protected BeanId wireBeanId;
protected BeanRuntimeInfo beanRuntimeInfo;
protected BeanRuntimeInfo wiredBeanRuntimeInfo;
protected Method propertySetterMethod;
protected boolean checkedForSetterMethod;
protected boolean isAttribute = true;
protected TypeConverterFactory, ?> typeConverterFactory;
protected String mapKeyAttribute;
protected boolean isBeanWiring;
protected BeanWiringObserver wireByBeanIdObserver;
protected ListToArrayChangeObserver listToArrayChangeObserver;
public ResourceConfig getConfig() {
return config;
}
public void setBeanId(String beanId) {
this.beanIdName = beanId;
}
public String getBeanId() {
return beanIdName;
}
public void setWireBeanId(String wireBeanId) {
this.wireBeanIdName = Optional.ofNullable(wireBeanId);
}
public String getWireBeanId() {
return wireBeanIdName.orElse(null);
}
public void setExpression(MVELExpressionEvaluator expression) {
this.expressionEvaluator = expression;
}
public void setProperty(String property) {
this.property = Optional.ofNullable(property);
}
public String getProperty() {
return property.orElse(null);
}
public void setSetterMethod(String setterMethod) {
this.setterMethod = Optional.ofNullable(setterMethod);
}
public void setValueAttributeName(String valueAttributeName) {
this.valueAttributeName = Optional.ofNullable(valueAttributeName);
}
public void setValueAttributePrefix(String valueAttributePrefix) {
this.valueAttributePrefix = Optional.ofNullable(valueAttributePrefix);
}
public void setTypeAlias(String typeAlias) {
this.typeAlias = Optional.ofNullable(typeAlias);
}
public void setTypeConverterFactory(TypeConverterFactory, ?> typeConverterFactory) {
this.typeConverterFactory = typeConverterFactory;
}
public TypeConverterFactory, ?> getTypeConverterFactory() {
return typeConverterFactory;
}
public void setDefaultVal(String defaultVal) {
this.defaultVal = Optional.ofNullable(defaultVal);
}
public boolean isBeanWiring() {
return isBeanWiring;
}
/**
* Set the resource configuration on the bean populator.
*
* @throws SmooksConfigException Incorrectly configured resource.
*/
@PostConstruct
public void postConstruct() throws SmooksConfigException {
buildId();
beanRuntimeInfo = BeanRuntimeInfo.getBeanRuntimeInfo(beanIdName, applicationContext);
isBeanWiring = wireBeanIdName.isPresent() || wireBeanType.isPresent() || wireBeanAnnotation.isPresent();
isAttribute = valueAttributeName.isPresent();
if (valueAttributePrefix.isPresent()) {
Optional namespaces = applicationContext.getRegistry().lookup(new NamespaceManagerLookup());
valueAttributeNS = namespaces.orElse(new Properties()).getProperty(valueAttributePrefix.get());
}
beanIdStore = applicationContext.getBeanIdStore();
beanId = beanIdStore.getBeanId(beanIdName);
if (!setterMethod.isPresent() && !property.isPresent()) {
if (isBeanWiring && (beanRuntimeInfo.getClassification() == BeanRuntimeInfo.Classification.NON_COLLECTION || beanRuntimeInfo.getClassification() == BeanRuntimeInfo.Classification.MAP_COLLECTION)) {
// Default the property name if it's a wiring...
property = Optional.of(wireBeanIdName.get());
} else if (beanRuntimeInfo.getClassification() == BeanRuntimeInfo.Classification.NON_COLLECTION) {
throw new SmooksConfigException("Binding configuration for beanIdName='" + beanIdName + "' must contain " +
"either a 'property' or 'setterMethod' attribute definition, unless the target bean is a Collection/Array." +
" Bean is type '" + beanRuntimeInfo.getPopulateType().getName() + "'.");
}
}
if (beanRuntimeInfo.getClassification() == BeanRuntimeInfo.Classification.MAP_COLLECTION && property.isPresent()) {
property = Optional.of(property.get().trim());
if (property.get().length() > 1 && property.get().charAt(0) == '@') {
mapKeyAttribute = property.get().substring(1);
}
}
if (expression.isPresent()) {
expression = Optional.of(expression.get().trim());
expressionHasDataVariable = expression.get().contains(EXPRESSION_VALUE_VARIABLE_NAME);
expression = Optional.of(expression.get().replace("this.", beanIdName + "."));
if (expression.get().startsWith("+=")) {
expression = Optional.of(beanIdName + "." + property.orElse(null) + " +" + expression.get().substring(2));
}
if (expression.get().startsWith("-=")) {
expression = Optional.of(beanIdName + "." + property.orElse(null) + " -" + expression.get().substring(2));
}
expressionEvaluator = new MVELExpressionEvaluator();
expressionEvaluator.setExpression(expression.get());
// If we can determine the target binding type, tell MVEL.
// If there's a decoder (a typeAlias), we define a String var instead and leave decoding
// to the decoder...
Class> bindingType = resolveBindTypeReflectively();
if (bindingType != null) {
if (typeAlias.isPresent()) {
bindingType = String.class;
}
expressionEvaluator.setToType(bindingType);
}
}
if (wireBeanIdName.isPresent()) {
wireBeanId = beanIdStore.getBeanId(wireBeanIdName.get());
if (wireBeanId == null) {
wireBeanId = beanIdStore.register(wireBeanIdName.get());
}
}
if (isBeanWiring) {
// These observers can be used concurrently across multiple execution contexts...
wireByBeanIdObserver = new BeanWiringObserver(beanId, this).watchedBeanId(wireBeanId).watchedBeanType(wireBeanType.orElse(null)).watchedBeanAnnotation(wireBeanAnnotation.orElse(null));
if (wireBeanId != null) {
// List to array change observer only makes sense if wiring by beanId.
listToArrayChangeObserver = new ListToArrayChangeObserver(wireBeanId, property.orElse(null), this);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Bean Instance Populator created for [" + beanIdName + "]. property=" + property.orElse(null));
}
}
protected void buildId() {
StringBuilder idBuilder = new StringBuilder();
idBuilder.append(BeanInstancePopulator.class.getName());
idBuilder.append("#");
idBuilder.append(beanIdName);
property.ifPresent(s -> idBuilder.append("#")
.append(s));
setterMethod.ifPresent(s -> idBuilder.append("#")
.append(s)
.append("()"));
wireBeanIdName.ifPresent(s -> idBuilder.append("#")
.append(s));
id = idBuilder.toString();
}
@Override
public void visitBefore(Element element, ExecutionContext executionContext) throws SmooksException {
if (!beanExists(executionContext)) {
LOGGER.debug("Cannot bind data onto bean '" + beanId + "' as bean does not exist in BeanContext.");
return;
}
if (isBeanWiring) {
bindBeanValue(executionContext, new NodeFragment(element));
} else if (isAttribute) {
// Bind attribute (i.e. selectors with '@' prefix) values on the visitBefore...
bindSaxDataValue(element, executionContext);
}
}
@Override
public void visitAfter(Element element, ExecutionContext executionContext) throws SmooksException {
if (!beanExists(executionContext)) {
LOGGER.debug("Cannot bind data onto bean '" + beanId + "' as bean does not exist in BeanContext.");
return;
}
if (!isBeanWiring && !isAttribute) {
bindSaxDataValue(element, executionContext);
}
}
protected boolean beanExists(ExecutionContext executionContext) {
return (executionContext.getBeanContext().getBean(beanId) != null);
}
protected void bindSaxDataValue(Element element, ExecutionContext executionContext) {
String propertyName;
if (mapKeyAttribute != null) {
propertyName = getAttributeValue(element, mapKeyAttribute, null);
if (propertyName == null) {
propertyName = element.getLocalName();
}
} else {
propertyName = property.orElseGet(element::getLocalName);
}
String dataString = null;
if (expressionEvaluator == null || expressionHasDataVariable) {
if (isAttribute) {
dataString = getAttributeValue(element, valueAttributeName.orElse(null), valueAttributeNS);
} else {
TextAccumulatorMemento textAccumulatorMemento = new TextAccumulatorVisitorMemento(new NodeFragment(element), this);
executionContext.getMementoCaretaker().restore(textAccumulatorMemento);
dataString = textAccumulatorMemento.getText();
}
}
if (expressionEvaluator != null) {
bindExpressionValue(propertyName, dataString, executionContext, new NodeFragment(element));
} else {
decodeAndSetPropertyValue(propertyName, dataString, executionContext, new NodeFragment(element));
}
}
protected String getAttributeValue(Element element, String attributeName, String namespaceURI) {
return DomUtils.getAttributeValue(element, attributeName, namespaceURI);
}
protected void bindBeanValue(final ExecutionContext executionContext, Fragment source) {
final BeanContext beanContext = executionContext.getBeanContext();
Object bean = null;
if (wireBeanId != null) {
bean = beanContext.getBean(wireBeanId);
}
if (bean != null) {
if (!BeanWiringObserver.isMatchingBean(bean, wireBeanType.orElse(null), wireBeanAnnotation.orElse(null))) {
bean = null;
}
}
if (bean == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registering bean ADD wiring observer for wiring bean '" + wireBeanId + "' onto target bean '" + beanId.getName() + "'.");
}
// Register the observer which looks for the creation of the selected bean via its beanIdName...
beanContext.addObserver(wireByBeanIdObserver);
} else {
populateAndSetPropertyValue(bean, beanContext, wireBeanId, executionContext, source);
}
}
public void populateAndSetPropertyValue(Object bean, BeanContext beanContext, BeanId targetBeanId, final ExecutionContext executionContext, Fragment source) {
BeanRuntimeInfo wiredBeanRI = getWiredBeanRuntimeInfo();
// When this observer is triggered then we look if we got something we can set immediately or that we got an array collection.
// For an array collection, we need the array representation and not the list representation, so we register and observer that
// listens for the change from the list to the array...
if (wiredBeanRI != null && wiredBeanRI.getClassification() == BeanRuntimeInfo.Classification.ARRAY_COLLECTION) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registering bean CHANGE wiring observer for wiring bean '" + targetBeanId + "' onto target bean '" + beanId.getName() + "' after it has been converted from a List to an array.");
}
// Register an observer which looks for the change that the mutable list of the selected bean gets converted to an array. We
// can then set this array
beanContext.addObserver(listToArrayChangeObserver);
} else {
setPropertyValue(property.orElse(null), bean, executionContext, source);
}
}
protected void bindExpressionValue(String mapPropertyName, String dataString, ExecutionContext executionContext, Fragment source) {
Map beanMap = executionContext.getBeanContext().getBeanMap();
Map variables = new HashMap<>();
if (expressionHasDataVariable) {
variables.put(EXPRESSION_VALUE_VARIABLE_NAME, dataString);
}
Object dataObject = expressionEvaluator.exec(beanMap, variables);
decodeAndSetPropertyValue(mapPropertyName, dataObject, executionContext, source);
}
protected void decodeAndSetPropertyValue(String mapPropertyName, Object dataObject, ExecutionContext executionContext, Fragment source) {
if (dataObject instanceof String) {
setPropertyValue(mapPropertyName, decodeDataString((String) dataObject, executionContext), executionContext, source);
} else {
setPropertyValue(mapPropertyName, dataObject, executionContext, source);
}
}
@SuppressWarnings("unchecked")
public void setPropertyValue(String mapPropertyName, Object dataObject, ExecutionContext executionContext, Fragment source) {
if (dataObject == null) {
return;
}
Object bean = executionContext.getBeanContext().getBean(beanId);
BeanRuntimeInfo.Classification beanType = beanRuntimeInfo.getClassification();
createPropertySetterMethod(bean, dataObject.getClass());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Setting data object '" + wireBeanIdName.orElse(null) + "' (" + dataObject.getClass().getName() + ") on target bean '" + beanId + "'.");
}
// Set the data on the bean...
try {
if (propertySetterMethod != null) {
propertySetterMethod.invoke(bean, dataObject);
} else if (beanType == BeanRuntimeInfo.Classification.MAP_COLLECTION) {
((Map) bean).put(mapPropertyName, dataObject);
} else if (beanType == BeanRuntimeInfo.Classification.ARRAY_COLLECTION || beanType == BeanRuntimeInfo.Classification.COLLECTION_COLLECTION) {
((Collection) bean).add(dataObject);
} else {
if (setterMethod.isPresent()) {
throw new SmooksConfigException("Bean [" + beanIdName + "] configuration invalid. Bean setter method [" + setterMethod.get() + "(" + dataObject.getClass().getName() + ")] not found on type [" + beanRuntimeInfo.getPopulateType().getName() + "]. You may need to set a 'decoder' on the binding config.");
} else if (property.isPresent()) {
boolean throwException = true;
if (beanRuntimeInfo.isJAXBType() && getWiredBeanRuntimeInfo().getClassification() != BeanRuntimeInfo.Classification.NON_COLLECTION) {
// It's a JAXB collection type. If the wired in bean is created by a factory then it's most
// probable that there's no need to set the collection because the JAXB type is creating it lazily
// in the getter method. So... we're going to ignore this.
if (wireBeanId.getCreateResourceConfiguration().getParameter("beanFactory", String.class) != null) {
throwException = false;
}
}
if (throwException) {
throw new SmooksConfigException("Bean [" + beanIdName + "] configuration invalid. Bean setter method [" + ClassUtils.toSetterName(property.get()) + "(" + dataObject.getClass().getName() + ")] not found on type [" + beanRuntimeInfo.getPopulateType().getName() + "]. You may need to set a 'decoder' on the binding config.");
}
}
}
if (notifyPopulate) {
BeanContextLifecycleEvent event = new DefaultBeanContextLifecycleEvent(executionContext, source, BeanLifecycle.POPULATE, beanId, bean);
executionContext.getBeanContext().notifyObservers(event);
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new SmooksConfigException("Error invoking bean setter method [" + ClassUtils.toSetterName(property.orElse(null)) + "] on bean instance class type [" + bean.getClass() + "].", e);
}
}
protected void createPropertySetterMethod(Object bean, Class> parameter) {
if (!checkedForSetterMethod && propertySetterMethod == null) {
String methodName = null;
if (setterMethod.isPresent() && !setterMethod.get().trim().equals("")) {
methodName = setterMethod.get();
} else if (property.isPresent() && !property.get().trim().equals("")) {
methodName = ClassUtils.toSetterName(property.get());
}
if (methodName != null) {
propertySetterMethod = createPropertySetterMethod(bean, methodName, parameter);
}
checkedForSetterMethod = true;
}
}
/**
* Create the bean setter method instance for this visitor.
*
* @param setterName The setter method name.
* @return The bean setter method.
*/
protected synchronized Method createPropertySetterMethod(Object bean, String setterName, Class> setterParamType) {
if (propertySetterMethod == null) {
propertySetterMethod = BeanUtils.createSetterMethod(setterName, bean, setterParamType);
}
return propertySetterMethod;
}
protected Object decodeDataString(String dataString, ExecutionContext executionContext) throws TypeConverterException {
if ((dataString == null || dataString.isEmpty()) && defaultVal.isPresent()) {
if (defaultVal.get().equals("null")) {
return null;
}
dataString = defaultVal.get();
}
if (typeConverterFactory == null) {
typeConverterFactory = getTypeConverterFactory(executionContext);
}
TypeConverter typeConverter = typeConverterFactory.createTypeConverter();
try {
return typeConverter.convert(dataString);
} catch (TypeConverterException e) {
throw new TypeConverterException("Failed to decode binding value '" + dataString + "' for property '" + property + "' on bean '" + beanId.getName() + "'.", e);
}
}
protected TypeConverterFactory, ?> getTypeConverterFactory(ExecutionContext executionContext) throws TypeConverterException {
return getTypeConverterFactory(executionContext.getContentDeliveryRuntime().getContentDeliveryConfig());
}
public TypeConverterFactory, ?> getTypeConverterFactory(ContentDeliveryConfig contentDeliveryConfig) {
List> typeConverterFactories = contentDeliveryConfig.getObjects("decoder:" + typeAlias.orElse(null));
if (typeConverterFactories == null || typeConverterFactories.isEmpty()) {
if (typeAlias.isPresent()) {
typeConverterFactory = applicationContext.getRegistry().lookup(new NameTypeConverterFactoryLookup<>(typeAlias.get()));
} else {
typeConverterFactory = resolveDecoderReflectively();
}
} else if (!(typeConverterFactories.get(0) instanceof TypeConverterFactory)) {
throw new TypeConverterException("Configured type converter factory '" + typeAlias.orElse(null) + ":" + typeConverterFactories.get(0).getClass().getName() + "' is not an instance of " + TypeConverterFactory.class.getName());
} else {
typeConverterFactory = (TypeConverterFactory) typeConverterFactories.get(0);
}
if (typeConverterFactory instanceof PreprocessTypeConverterFactory) {
PreprocessTypeConverterFactory preprocessTypeConverterFactory = (PreprocessTypeConverterFactory) typeConverterFactory;
if (preprocessTypeConverterFactory.getDelegateTypeConverterFactory() == null) {
preprocessTypeConverterFactory.setDelegateTypeConverterFactory(resolveDecoderReflectively());
}
}
return typeConverterFactory;
}
protected TypeConverterFactory super String, ?> resolveDecoderReflectively() throws TypeConverterException {
Class> bindType = resolveBindTypeReflectively();
if (bindType != null) {
if (bindType.isEnum()) {
return new TypeConverterFactory() {
@Override
public TypeConverter super String, ?> createTypeConverter() {
return (TypeConverter) value -> Enum.valueOf((Class) bindType, value);
}
@Override
public TypeConverterDescriptor, Class
© 2015 - 2024 Weber Informatics LLC | Privacy Policy