http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java new file mode 100644 index 0000000..69e78b7 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2007,2009 The Apache Software Foundation. + * + * 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. + */ +package com.opensymphony.xwork2.conversion.impl; + +import com.opensymphony.xwork2.XWorkConstants; +import com.opensymphony.xwork2.XWorkException; +import com.opensymphony.xwork2.conversion.TypeConverter; +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.Inject; + +import java.lang.reflect.Member; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Map; + + +/** + * <!-- START SNIPPET: javadoc --> + * <p/> + * XWork will automatically handle the most common type conversion for you. This includes support for converting to + * and from Strings for each of the following: + * <p/> + * <ul> + * <li>String</li> + * <li>boolean / Boolean</li> + * <li>char / Character</li> + * <li>int / Integer, float / Float, long / Long, double / Double</li> + * <li>dates - uses the SHORT format for the Locale associated with the current request</li> + * <li>arrays - assuming the individual strings can be coverted to the individual items</li> + * <li>collections - if not object type can be determined, it is assumed to be a String and a new ArrayList is + * created</li> + * </ul> + * <p/> Note that with arrays the type conversion will defer to the type of the array elements and try to convert each + * item individually. As with any other type conversion, if the conversion can't be performed the standard type + * conversion error reporting is used to indicate a problem occurred while processing the type conversion. + * <!-- END SNIPPET: javadoc --> + * + * @author <a href="mailto:[email protected]">Pat Lightbody</a> + * @author Mike Mosiewicz + * @author Rainer Hermanns + * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a> + */ +public class XWorkBasicConverter extends DefaultTypeConverter { + + private Container container; + + @Inject + public void setContainer(Container container) { + this.container = container; + } + + @Override + public Object convertValue(Map<String, Object> context, Object o, Member member, String propertyName, Object value, Class toType) { + Object result = null; + + if (value == null || toType.isAssignableFrom(value.getClass())) { + // no need to convert at all, right? + return value; + } + + if (toType == String.class) { + /* the code below has been disabled as it causes sideffects in Struts2 (XW-512) + // if input (value) is a number then use special conversion method (XW-490) + Class inputType = value.getClass(); + if (Number.class.isAssignableFrom(inputType)) { + result = doConvertFromNumberToString(context, value, inputType); + if (result != null) { + return result; + } + }*/ + // okay use default string conversion + result = doConvertToString(context, value); + } else if (toType == boolean.class) { + result = doConvertToBoolean(value); + } else if (toType == Boolean.class) { + result = doConvertToBoolean(value); + } else if (toType.isArray()) { + result = doConvertToArray(context, o, member, propertyName, value, toType); + } else if (Date.class.isAssignableFrom(toType)) { + result = doConvertToDate(context, value, toType); + } else if (Calendar.class.isAssignableFrom(toType)) { + result = doConvertToCalendar(context, value); + } else if (Collection.class.isAssignableFrom(toType)) { + result = doConvertToCollection(context, o, member, propertyName, value, toType); + } else if (toType == Character.class) { + result = doConvertToCharacter(value); + } else if (toType == char.class) { + result = doConvertToCharacter(value); + } else if (Number.class.isAssignableFrom(toType) || toType.isPrimitive()) { + result = doConvertToNumber(context, value, toType); + } else if (toType == Class.class) { + result = doConvertToClass(value); + } + + if (result == null) { + if (value instanceof Object[]) { + Object[] array = (Object[]) value; + + if (array.length >= 1) { + value = array[0]; + } else { + value = null; + } + + // let's try to convert the first element only + result = convertValue(context, o, member, propertyName, value, toType); + } else if (!"".equals(value)) { // we've already tried the types we know + result = super.convertValue(context, value, toType); + } + + if (result == null && value != null && !"".equals(value)) { + throw new XWorkException("Cannot create type " + toType + " from value " + value); + } + } + + return result; + } + + private Object doConvertToCalendar(Map<String, Object> context, Object value) { + Object result = null; + Date dateResult = (Date) doConvertToDate(context, value, Date.class); + if (dateResult != null) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(dateResult); + result = calendar; + } + return result; + } + + private Object doConvertToCharacter(Object value) { + if (value instanceof String) { + String cStr = (String) value; + return (cStr.length() > 0) ? cStr.charAt(0) : null; + } + return null; + } + + private Object doConvertToBoolean(Object value) { + if (value instanceof String) { + String bStr = (String) value; + return Boolean.valueOf(bStr); + } + return null; + } + + private Class doConvertToClass(Object value) { + Class clazz = null; + if (value != null && value instanceof String && ((String) value).length() > 0) { + try { + clazz = Class.forName((String) value); + } catch (ClassNotFoundException e) { + throw new XWorkException(e.getLocalizedMessage(), e); + } + } + return clazz; + } + + private Object doConvertToCollection(Map<String, Object> context, Object o, Member member, String prop, Object value, Class toType) { + TypeConverter converter = container.getInstance(CollectionConverter.class); + if (converter == null) { + throw new XWorkException("TypeConverter with name [#0] must be registered first!", XWorkConstants.COLLECTION_CONVERTER); + } + return converter.convertValue(context, o, member, prop, value, toType); + } + + private Object doConvertToArray(Map<String, Object> context, Object o, Member member, String prop, Object value, Class toType) { + TypeConverter converter = container.getInstance(ArrayConverter.class); + if (converter == null) { + throw new XWorkException("TypeConverter with name [#0] must be registered first!", XWorkConstants.ARRAY_CONVERTER); + } + return converter.convertValue(context, o, member, prop, value, toType); + } + + private Object doConvertToDate(Map<String, Object> context, Object value, Class toType) { + TypeConverter converter = container.getInstance(DateConverter.class); + if (converter == null) { + throw new XWorkException("TypeConverter with name [#0] must be registered first!", XWorkConstants.DATE_CONVERTER); + } + return converter.convertValue(context, null, null, null, value, toType); + } + + private Object doConvertToNumber(Map<String, Object> context, Object value, Class toType) { + TypeConverter converter = container.getInstance(NumberConverter.class); + if (converter == null) { + throw new XWorkException("TypeConverter with name [#0] must be registered first!", XWorkConstants.NUMBER_CONVERTER); + } + return converter.convertValue(context, null, null, null, value, toType); + } + + private Object doConvertToString(Map<String, Object> context, Object value) { + TypeConverter converter = container.getInstance(StringConverter.class); + if (converter == null) { + throw new XWorkException("TypeConverter with name [#0] must be registered first!", XWorkConstants.STRING_CONVERTER); + } + return converter.convertValue(context, null, null, null, value, null); + } + +}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java new file mode 100644 index 0000000..c0086ee --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java @@ -0,0 +1,595 @@ +/* + * Copyright 2002-2006,2009 The Apache Software Foundation. + * + * 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. + */ +package com.opensymphony.xwork2.conversion.impl; + +import com.opensymphony.xwork2.*; +import com.opensymphony.xwork2.conversion.*; +import com.opensymphony.xwork2.conversion.annotations.Conversion; +import com.opensymphony.xwork2.conversion.annotations.TypeConversion; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.*; +import com.opensymphony.xwork2.util.reflection.ReflectionContextState; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.net.URL; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * XWorkConverter is a singleton used by many of the Struts 2's Ognl extention points, + * such as InstantiatingNullHandler, XWorkListPropertyAccessor etc to do object + * conversion. + * <p/> + * <!-- START SNIPPET: javadoc --> + * <p/> + * Type conversion is great for situations where you need to turn a String in to a more complex object. Because the web + * is type-agnostic (everything is a string in HTTP), Struts 2's type conversion features are very useful. For instance, + * if you were prompting a user to enter in coordinates in the form of a string (such as "3, 22"), you could have + * Struts 2 do the conversion both from String to Point and from Point to String. + * <p/> + * <p/> Using this "point" example, if your action (or another compound object in which you are setting properties on) + * has a corresponding ClassName-conversion.properties file, Struts 2 will use the configured type converters for + * conversion to and from strings. So turning "3, 22" in to new Point(3, 22) is done by merely adding the following + * entry to <b>ClassName-conversion.properties</b> (Note that the PointConverter should impl the TypeConverter + * interface): + * <p/> + * <p/><b>point = com.acme.PointConverter</b> + * <p/> + * <p/> Your type converter should be sure to check what class type it is being requested to convert. Because it is used + * for both to and from strings, you will need to split the conversion method in to two parts: one that turns Strings in + * to Points, and one that turns Points in to Strings. + * <p/> + * <p/> After this is done, you can now reference your point (using <s:property value="point"/> in JSP or ${point} + * in FreeMarker) and it will be printed as "3, 22" again. As such, if you submit this back to an action, it will be + * converted back to a Point once again. + * <p/> + * <p/> In some situations you may wish to apply a type converter globally. This can be done by editing the file + * <b>xwork-conversion.properties</b> in the root of your class path (typically WEB-INF/classes) and providing a + * property in the form of the class name of the object you wish to convert on the left hand side and the class name of + * the type converter on the right hand side. For example, providing a type converter for all Point objects would mean + * adding the following entry: + * <p/> + * <p/><b>com.acme.Point = com.acme.PointConverter</b> + * <p/> + * <!-- END SNIPPET: javadoc --> + * <p/> + * <p/> + * <p/> + * <!-- START SNIPPET: i18n-note --> + * <p/> + * Type conversion should not be used as a substitute for i18n. It is not recommended to use this feature to print out + * properly formatted dates. Rather, you should use the i18n features of Struts 2 (and consult the JavaDocs for JDK's + * MessageFormat object) to see how a properly formatted date should be displayed. + * <p/> + * <!-- END SNIPPET: i18n-note --> + * <p/> + * <p/> + * <p/> + * <!-- START SNIPPET: error-reporting --> + * <p/> + * Any error that occurs during type conversion may or may not wish to be reported. For example, reporting that the + * input "abc" could not be converted to a number might be important. On the other hand, reporting that an empty string, + * "", cannot be converted to a number might not be important - especially in a web environment where it is hard to + * distinguish between a user not entering a value vs. entering a blank value. + * <p/> + * <p/> By default, all conversion errors are reported using the generic i18n key <b>xwork.default.invalid.fieldvalue</b>, + * which you can override (the default text is <i>Invalid field value for field "xxx"</i>, where xxx is the field name) + * in your global i18n resource bundle. + * <p/> + * <p/>However, sometimes you may wish to override this message on a per-field basis. You can do this by adding an i18n + * key associated with just your action (Action.properties) using the pattern <b>invalid.fieldvalue.xxx</b>, where xxx + * is the field name. + * <p/> + * <p/>It is important to know that none of these errors are actually reported directly. Rather, they are added to a map + * called <i>conversionErrors</i> in the ActionContext. There are several ways this map can then be accessed and the + * errors can be reported accordingly. + * <p/> + * <!-- END SNIPPET: error-reporting --> + * + * @author <a href="mailto:[email protected]">Pat Lightbody</a> + * @author Rainer Hermanns + * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a> + * @author tm_jee + * @version $Date$ $Id$ + * @see XWorkBasicConverter + */ +public class XWorkConverter extends DefaultTypeConverter { + + private static final Logger LOG = LogManager.getLogger(XWorkConverter.class); + + public static final String REPORT_CONVERSION_ERRORS = "report.conversion.errors"; + public static final String CONVERSION_PROPERTY_FULLNAME = "conversion.property.fullName"; + public static final String CONVERSION_ERROR_PROPERTY_PREFIX = "invalid.fieldvalue."; + public static final String CONVERSION_COLLECTION_PREFIX = "Collection_"; + + public static final String LAST_BEAN_CLASS_ACCESSED = "last.bean.accessed"; + public static final String LAST_BEAN_PROPERTY_ACCESSED = "last.property.accessed"; + public static final String MESSAGE_INDEX_PATTERN = "\\[\\d+\\]\\."; + public static final String MESSAGE_INDEX_BRACKET_PATTERN = "[\\[\\]\\.]"; + public static final String PERIOD = "."; + public static final Pattern messageIndexPattern = Pattern.compile(MESSAGE_INDEX_PATTERN); + + private TypeConverter defaultTypeConverter; + private FileManager fileManager; + private boolean reloadingConfigs; + + private ConversionFileProcessor fileProcessor; + private ConversionAnnotationProcessor annotationProcessor; + + private TypeConverterHolder converterHolder; + + protected XWorkConverter() { + } + + @Inject + public void setDefaultTypeConverter(XWorkBasicConverter converter) { + this.defaultTypeConverter = converter; + } + + @Inject + public void setFileManagerFactory(FileManagerFactory fileManagerFactory) { + this.fileManager = fileManagerFactory.getFileManager(); + } + + @Inject(value = XWorkConstants.RELOAD_XML_CONFIGURATION, required = false) + public void setReloadingConfigs(String reloadingConfigs) { + this.reloadingConfigs = Boolean.parseBoolean(reloadingConfigs); + } + + @Inject + public void setConversionPropertiesProcessor(ConversionPropertiesProcessor propertiesProcessor) { + // note: this file is deprecated + propertiesProcessor.process("xwork-default-conversion.properties"); + propertiesProcessor.process("xwork-conversion.properties"); + } + + @Inject + public void setConversionFileProcessor(ConversionFileProcessor fileProcessor) { + this.fileProcessor = fileProcessor; + } + + @Inject + public void setConversionAnnotationProcessor(ConversionAnnotationProcessor annotationProcessor) { + this.annotationProcessor = annotationProcessor; + } + + @Inject + public void setTypeConverterHolder(TypeConverterHolder converterHolder) { + this.converterHolder = converterHolder; + } + + public static String getConversionErrorMessage(String propertyName, ValueStack stack) { + String defaultMessage = LocalizedTextUtil.findDefaultText(XWorkMessages.DEFAULT_INVALID_FIELDVALUE, + ActionContext.getContext().getLocale(), + new Object[]{ + propertyName + }); + + List<String> indexValues = getIndexValues(propertyName); + + propertyName = removeAllIndexesInPropertyName(propertyName); + + String getTextExpression = "getText('" + CONVERSION_ERROR_PROPERTY_PREFIX + propertyName + "','" + defaultMessage + "')"; + String message = (String) stack.findValue(getTextExpression); + + if (message == null) { + message = defaultMessage; + } else { + message = MessageFormat.format(message, indexValues.toArray()); + } + + return message; + } + + private static String removeAllIndexesInPropertyName(String propertyName) { + return propertyName.replaceAll(MESSAGE_INDEX_PATTERN, PERIOD); + } + + private static List<String> getIndexValues(String propertyName) { + Matcher matcher = messageIndexPattern.matcher(propertyName); + List<String> indexes = new ArrayList<>(); + while (matcher.find()) { + Integer index = new Integer(matcher.group().replaceAll(MESSAGE_INDEX_BRACKET_PATTERN, "")) + 1; + indexes.add(Integer.toString(index)); + } + return indexes; + } + + public String buildConverterFilename(Class clazz) { + String className = clazz.getName(); + return className.replace('.', '/') + "-conversion.properties"; + } + + @Override + public Object convertValue(Map<String, Object> map, Object o, Class aClass) { + return convertValue(map, null, null, null, o, aClass); + } + + /** + * Convert value from one form to another. + * Minimum requirement of arguments: + * <ul> + * <li>supplying context, toClass and value</li> + * <li>supplying context, target and value.</li> + * </ul> + * + * @see TypeConverter#convertValue(java.util.Map, java.lang.Object, java.lang.reflect.Member, java.lang.String, java.lang.Object, java.lang.Class) + */ + @Override + public Object convertValue(Map<String, Object> context, Object target, Member member, String property, Object value, Class toClass) { + // + // Process the conversion using the default mappings, if one exists + // + TypeConverter tc = null; + + if ((value != null) && (toClass == value.getClass())) { + return value; + } + + // allow this method to be called without any context + // i.e. it can be called with as little as "Object value" and "Class toClass" + if (target != null) { + Class clazz = target.getClass(); + + Object[] classProp = null; + + // this is to handle weird issues with setValue with a different type + if ((target instanceof CompoundRoot) && (context != null)) { + classProp = getClassProperty(context); + } + + if (classProp != null) { + clazz = (Class) classProp[0]; + property = (String) classProp[1]; + } + + tc = (TypeConverter) getConverter(clazz, property); + LOG.debug("field-level type converter for property [{}] = {}", property, (tc == null ? "none found" : tc)); + } + + if (tc == null && context != null) { + // ok, let's see if we can look it up by path as requested in XW-297 + Object lastPropertyPath = context.get(ReflectionContextState.CURRENT_PROPERTY_PATH); + Class clazz = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED); + if (lastPropertyPath != null && clazz != null) { + String path = lastPropertyPath + "." + property; + tc = (TypeConverter) getConverter(clazz, path); + } + } + + if (tc == null) { + if (toClass.equals(String.class) && (value != null) && !(value.getClass().equals(String.class) || value.getClass().equals(String[].class))) { + // when converting to a string, use the source target's class's converter + tc = lookup(value.getClass()); + } else { + // when converting from a string, use the toClass's converter + tc = lookup(toClass); + } + + if (LOG.isDebugEnabled()) + LOG.debug("global-level type converter for property [{}] = {} ", property, (tc == null ? "none found" : tc)); + } + + + if (tc != null) { + try { + return tc.convertValue(context, target, member, property, value, toClass); + } catch (Exception e) { + LOG.debug("Unable to convert value using type converter [{}]", tc.getClass().getName(), e); + handleConversionException(context, property, value, target); + + return TypeConverter.NO_CONVERSION_POSSIBLE; + } + } + + if (defaultTypeConverter != null) { + try { + LOG.debug("Falling back to default type converter [{}]", defaultTypeConverter); + return defaultTypeConverter.convertValue(context, target, member, property, value, toClass); + } catch (Exception e) { + LOG.debug("Unable to convert value using type converter [{}]", defaultTypeConverter.getClass().getName(), e); + handleConversionException(context, property, value, target); + + return TypeConverter.NO_CONVERSION_POSSIBLE; + } + } else { + try { + LOG.debug("Falling back to Ognl's default type conversion"); + return super.convertValue(value, toClass); + } catch (Exception e) { + LOG.debug("Unable to convert value using type converter [{}]", super.getClass().getName(), e); + handleConversionException(context, property, value, target); + + return TypeConverter.NO_CONVERSION_POSSIBLE; + } + } + } + + /** + * Looks for a TypeConverter in the default mappings. + * + * @param className name of the class the TypeConverter must handle + * @return a TypeConverter to handle the specified class or null if none can be found + */ + public TypeConverter lookup(String className, boolean isPrimitive) { + if (converterHolder.containsUnknownMapping(className) && !converterHolder.containsDefaultMapping(className)) { + return null; + } + + TypeConverter result = converterHolder.getDefaultMapping(className); + + //Looks for super classes + if (result == null && !isPrimitive) { + Class clazz = null; + + try { + clazz = Thread.currentThread().getContextClassLoader().loadClass(className); + } catch (ClassNotFoundException cnfe) { + LOG.debug("Cannot load class {}", className, cnfe); + } + + result = lookupSuper(clazz); + + if (result != null) { + //Register now, the next lookup will be faster + registerConverter(className, result); + } else { + // if it isn't found, never look again (also faster) + registerConverterNotFound(className); + } + } + + return result; + } + + /** + * Looks for a TypeConverter in the default mappings. + * + * @param clazz the class the TypeConverter must handle + * @return a TypeConverter to handle the specified class or null if none can be found + */ + public TypeConverter lookup(Class clazz) { + TypeConverter result = lookup(clazz.getName(), clazz.isPrimitive()); + + if (result == null && clazz.isPrimitive()) { + /** + * if it is primitive use default converter which allows to define different converters per type + * @see XWorkBasicConverter + */ + return defaultTypeConverter; + } + + return result; + } + + protected Object getConverter(Class clazz, String property) { + LOG.debug("Retrieving convert for class [{}] and property [{}]", clazz, property); + + synchronized (clazz) { + if ((property != null) && !converterHolder.containsNoMapping(clazz)) { + try { + Map<String, Object> mapping = converterHolder.getMapping(clazz); + + if (mapping == null) { + mapping = buildConverterMapping(clazz); + } else { + mapping = conditionalReload(clazz, mapping); + } + + Object converter = mapping.get(property); + if (converter == null && LOG.isDebugEnabled()) { + LOG.debug("Converter is null for property [{}]. Mapping size [{}]:", property, mapping.size()); + for (String next : mapping.keySet()) { + LOG.debug("{}:{}", next, mapping.get(next)); + } + } + return converter; + } catch (Throwable t) { + LOG.debug("Got exception trying to resolve convert for class [{}] and property [{}]", clazz, property, t); + converterHolder.addNoMapping(clazz); + } + } + } + return null; + } + + protected void handleConversionException(Map<String, Object> context, String property, Object value, Object object) { + if (context != null && (Boolean.TRUE.equals(context.get(REPORT_CONVERSION_ERRORS)))) { + String realProperty = property; + String fullName = (String) context.get(CONVERSION_PROPERTY_FULLNAME); + + if (fullName != null) { + realProperty = fullName; + } + + Map<String, Object> conversionErrors = (Map<String, Object>) context.get(ActionContext.CONVERSION_ERRORS); + + if (conversionErrors == null) { + conversionErrors = new HashMap<>(); + context.put(ActionContext.CONVERSION_ERRORS, conversionErrors); + } + + conversionErrors.put(realProperty, value); + } + } + + public synchronized void registerConverter(String className, TypeConverter converter) { + converterHolder.addDefaultMapping(className, converter); + } + + public synchronized void registerConverterNotFound(String className) { + converterHolder.addUnknownMapping(className); + } + + private Object[] getClassProperty(Map<String, Object> context) { + Object lastClass = context.get(LAST_BEAN_CLASS_ACCESSED); + Object lastProperty = context.get(LAST_BEAN_PROPERTY_ACCESSED); + return (lastClass != null && lastProperty != null) ? new Object[] {lastClass, lastProperty} : null; + } + + /** + * Looks for converter mappings for the specified class and adds it to an existing map. Only new converters are + * added. If a converter is defined on a key that already exists, the converter is ignored. + * + * @param mapping an existing map to add new converter mappings to + * @param clazz class to look for converter mappings for + */ + protected void addConverterMapping(Map<String, Object> mapping, Class clazz) { + // Process <clazz>-conversion.properties file + String converterFilename = buildConverterFilename(clazz); + fileProcessor.process(mapping, clazz, converterFilename); + + // Process annotations + Annotation[] annotations = clazz.getAnnotations(); + + for (Annotation annotation : annotations) { + if (annotation instanceof Conversion) { + Conversion conversion = (Conversion) annotation; + for (TypeConversion tc : conversion.conversions()) { + if (mapping.containsKey(tc.key())) { + break; + } + if (LOG.isDebugEnabled()) { + if (StringUtils.isEmpty(tc.key())) { + LOG.debug("WARNING! key of @TypeConversion [{}] applied to [{}] is empty!", tc.converter(), clazz.getName()); + } else { + LOG.debug("TypeConversion [{}] with key: [{}]", tc.converter(), tc.key()); + } + } + annotationProcessor.process(mapping, tc, tc.key()); + } + } + } + + // Process annotated methods + for (Method method : clazz.getMethods()) { + annotations = method.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation instanceof TypeConversion) { + TypeConversion tc = (TypeConversion) annotation; + if (mapping.containsKey(tc.key())) { + break; + } + String key = tc.key(); + // Default to the property name + if (StringUtils.isEmpty(key)) { + key = AnnotationUtils.resolvePropertyName(method); + LOG.debug("Retrieved key [{}] from method name [{}]", key, method.getName()); + } + annotationProcessor.process(mapping, tc, key); + } + } + } + } + + + /** + * Looks for converter mappings for the specified class, traversing up its class hierarchy and interfaces and adding + * any additional mappings it may find. Mappings lower in the hierarchy have priority over those higher in the + * hierarcy. + * + * @param clazz the class to look for converter mappings for + * @return the converter mappings + */ + protected Map<String, Object> buildConverterMapping(Class clazz) throws Exception { + Map<String, Object> mapping = new HashMap<>(); + + // check for conversion mapping associated with super classes and any implemented interfaces + Class curClazz = clazz; + + while (!curClazz.equals(Object.class)) { + // add current class' mappings + addConverterMapping(mapping, curClazz); + + // check interfaces' mappings + Class[] interfaces = curClazz.getInterfaces(); + + for (Class anInterface : interfaces) { + addConverterMapping(mapping, anInterface); + } + + curClazz = curClazz.getSuperclass(); + } + + if (mapping.size() > 0) { + converterHolder.addMapping(clazz, mapping); + } else { + converterHolder.addNoMapping(clazz); + } + + return mapping; + } + + private Map<String, Object> conditionalReload(Class clazz, Map<String, Object> oldValues) throws Exception { + Map<String, Object> mapping = oldValues; + + if (reloadingConfigs) { + URL fileUrl = ClassLoaderUtil.getResource(buildConverterFilename(clazz), clazz); + if (fileManager.fileNeedsReloading(fileUrl)) { + mapping = buildConverterMapping(clazz); + } + } + + return mapping; + } + + /** + * Recurses through a class' interfaces and class hierarchy looking for a TypeConverter in the default mapping that + * can handle the specified class. + * + * @param clazz the class the TypeConverter must handle + * @return a TypeConverter to handle the specified class or null if none can be found + */ + TypeConverter lookupSuper(Class clazz) { + TypeConverter result = null; + + if (clazz != null) { + result = converterHolder.getDefaultMapping(clazz.getName()); + + if (result == null) { + // Looks for direct interfaces (depth = 1 ) + Class[] interfaces = clazz.getInterfaces(); + + for (Class anInterface : interfaces) { + if (converterHolder.containsDefaultMapping(anInterface.getName())) { + result = converterHolder.getDefaultMapping(anInterface.getName()); + break; + } + } + + if (result == null) { + // Looks for the superclass + // If 'clazz' is the Object class, an interface, a primitive type or void then clazz.getSuperClass() returns null + result = lookupSuper(clazz.getSuperclass()); + } + } + } + + return result; + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/ConversionDescription.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/ConversionDescription.java b/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/ConversionDescription.java new file mode 100644 index 0000000..340dc84 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/ConversionDescription.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2006,2009 The Apache Software Foundation. + * + * 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. + */ +package com.opensymphony.xwork2.conversion.metadata; + +import com.opensymphony.xwork2.conversion.annotations.ConversionRule; +import com.opensymphony.xwork2.conversion.impl.DefaultObjectTypeDeterminer; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * <code>ConversionDescription</code> + * + * @author Rainer Hermanns + * @version $Id$ + */ +public class ConversionDescription { + + /** + * Jakarta commons-logging reference. + */ + protected static Logger log = null; + + + public static final String KEY_PREFIX = "Key_"; + public static final String ELEMENT_PREFIX = "Element_"; + public static final String KEY_PROPERTY_PREFIX = "KeyProperty_"; + public static final String DEPRECATED_ELEMENT_PREFIX = "Collection_"; + + /** + * Key used for type conversion of maps. + */ + String MAP_PREFIX = "Map_"; + + public String property; + public String typeConverter = ""; + public String rule = ""; + public String value = ""; + public String fullQualifiedClassName; + public String type = null; + + public ConversionDescription() { + log = LogManager.getLogger(this.getClass()); + } + + /** + * Creates an ConversionDescription with the specified property name. + * + * @param property + */ + public ConversionDescription(String property) { + this.property = property; + log = LogManager.getLogger(this.getClass()); + } + + /** + * <p> + * Sets the property name to be inserted into the related conversion.properties file.<br/> + * Note: Do not add COLLECTION_PREFIX or MAP_PREFIX keys to property names. + * </p> + * + * @param property The property to be converted. + */ + public void setProperty(String property) { + this.property = property; + } + + /** + * Sets the class name of the type converter to be used. + * + * @param typeConverter The class name of the type converter. + */ + public void setTypeConverter(String typeConverter) { + this.typeConverter = typeConverter; + } + + /** + * Sets the rule prefix for COLLECTION_PREFIX or MAP_PREFIX key. + * Defaults to en emtpy String. + * + * @param rule + */ + public void setRule(String rule) { + if (rule != null && rule.length() > 0) { + if (rule.equals(ConversionRule.COLLECTION.toString())) { + this.rule = DefaultObjectTypeDeterminer.DEPRECATED_ELEMENT_PREFIX; + } else if (rule.equals(ConversionRule.ELEMENT.toString())) { + this.rule = DefaultObjectTypeDeterminer.ELEMENT_PREFIX; + } else if (rule.equals(ConversionRule.KEY.toString())) { + this.rule = DefaultObjectTypeDeterminer.KEY_PREFIX; + } else if (rule.equals(ConversionRule.KEY_PROPERTY.toString())) { + this.rule = DefaultObjectTypeDeterminer.KEY_PROPERTY_PREFIX; + } else if (rule.equals(ConversionRule.MAP.toString())) { + this.rule = MAP_PREFIX; + } + } + } + + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + /** + * Returns the conversion description as property entry. + * <p> + * Example:<br/> + * property.name = converter.className<br/> + * Collection_property.name = converter.className<br/> + * Map_property.name = converter.className + * KeyProperty_name = id + * </p> + * + * @return the conversion description as property entry. + */ + public String asProperty() { + StringWriter sw = new StringWriter(); + PrintWriter writer = null; + try { + writer = new PrintWriter(sw); + writer.print(rule); + writer.print(property); + writer.print("="); + if ( rule.startsWith(DefaultObjectTypeDeterminer.KEY_PROPERTY_PREFIX) && value != null && value.length() > 0 ) { + writer.print(value); + } else { + writer.print(typeConverter); + } + } finally { + if (writer != null) { + writer.flush(); + writer.close(); + } + } + + return sw.toString(); + + } + + /** + * Returns the fullQualifiedClassName attribute is used to create the special <code>conversion.properties</code> file name. + * + * @return fullQualifiedClassName + */ + public String getFullQualifiedClassName() { + return fullQualifiedClassName; + } + + /** + * The fullQualifiedClassName attribute is used to create the special <code>conversion.properties</code> file name. + * + * @param fullQualifiedClassName + */ + public void setFullQualifiedClassName(String fullQualifiedClassName) { + this.fullQualifiedClassName = fullQualifiedClassName; + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/package.html ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/package.html b/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/package.html new file mode 100644 index 0000000..c50a611 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/metadata/package.html @@ -0,0 +1 @@ +<body>Type conversion meta data classes.</body> http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/ActionFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/ActionFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/ActionFactory.java new file mode 100644 index 0000000..d33cb1d --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/ActionFactory.java @@ -0,0 +1,18 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.config.entities.ActionConfig; + +import java.util.Map; + +/** + * Used by {@link com.opensymphony.xwork2.ObjectFactory} to build actions + */ +public interface ActionFactory { + + /** + * Builds action instance + */ + Object buildAction(String actionName, String namespace, ActionConfig config, Map<String, Object> extraContext) throws Exception; + +} + http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/ConverterFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/ConverterFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/ConverterFactory.java new file mode 100644 index 0000000..289eefe --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/ConverterFactory.java @@ -0,0 +1,21 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.conversion.TypeConverter; + +import java.util.Map; + +/** + * Dedicated interface used by {@link com.opensymphony.xwork2.ObjectFactory} to build {@link TypeConverter} + */ +public interface ConverterFactory { + + /** + * Build converter of given type + * + * @param converterClass to instantiate + * @param extraContext a Map of extra context which uses the same keys as the {@link com.opensymphony.xwork2.ActionContext} + * @return instance of converterClass with inject dependencies + */ + TypeConverter buildConverter(Class<? extends TypeConverter> converterClass, Map<String, Object> extraContext) throws Exception; + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/DefaultActionFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/DefaultActionFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultActionFactory.java new file mode 100644 index 0000000..a17484a --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultActionFactory.java @@ -0,0 +1,25 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.inject.Inject; + +import java.util.Map; + +/** + * Default implementation + */ +public class DefaultActionFactory implements ActionFactory { + + private ObjectFactory objectFactory; + + @Inject + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + public Object buildAction(String actionName, String namespace, ActionConfig config, Map<String, Object> extraContext) throws Exception { + return objectFactory.buildBean(config.getClassName(), extraContext); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/DefaultConverterFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/DefaultConverterFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultConverterFactory.java new file mode 100644 index 0000000..e35222e --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultConverterFactory.java @@ -0,0 +1,30 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.conversion.TypeConverter; +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.Inject; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +import java.util.Map; + +/** + * Default implementation + */ +public class DefaultConverterFactory implements ConverterFactory { + + private static final Logger LOG = LogManager.getLogger(DefaultConverterFactory.class); + + private Container container; + + @Inject + public void setContainer(Container container) { + this.container = container; + } + + public TypeConverter buildConverter(Class<? extends TypeConverter> converterClass, Map<String, Object> extraContext) throws Exception { + LOG.debug("Creating converter of type [{}]", converterClass.getCanonicalName()); + return container.getInstance(converterClass); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/DefaultInterceptorFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/DefaultInterceptorFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultInterceptorFactory.java new file mode 100644 index 0000000..9f0195f --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultInterceptorFactory.java @@ -0,0 +1,67 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.config.ConfigurationException; +import com.opensymphony.xwork2.config.entities.InterceptorConfig; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.Interceptor; +import com.opensymphony.xwork2.util.reflection.ReflectionProvider; + +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation + */ +public class DefaultInterceptorFactory implements InterceptorFactory { + + private ObjectFactory objectFactory; + private ReflectionProvider reflectionProvider; + + @Inject + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + @Inject + public void setReflectionProvider(ReflectionProvider reflectionProvider) { + this.reflectionProvider = reflectionProvider; + } + + public Interceptor buildInterceptor(InterceptorConfig interceptorConfig, Map<String, String> interceptorRefParams) throws ConfigurationException { + String interceptorClassName = interceptorConfig.getClassName(); + Map<String, String> thisInterceptorClassParams = interceptorConfig.getParams(); + Map<String, String> params = (thisInterceptorClassParams == null) ? new HashMap<String, String>() : new HashMap<>(thisInterceptorClassParams); + params.putAll(interceptorRefParams); + + String message; + Throwable cause; + + try { + // interceptor instances are long-lived and used across user sessions, so don't try to pass in any extra context + Interceptor interceptor = (Interceptor) objectFactory.buildBean(interceptorClassName, null); + reflectionProvider.setProperties(params, interceptor); + interceptor.init(); + + return interceptor; + } catch (InstantiationException e) { + cause = e; + message = "Unable to instantiate an instance of Interceptor class [" + interceptorClassName + "]."; + } catch (IllegalAccessException e) { + cause = e; + message = "IllegalAccessException while attempting to instantiate an instance of Interceptor class [" + interceptorClassName + "]."; + } catch (ClassCastException e) { + cause = e; + message = "Class [" + interceptorClassName + "] does not implement com.opensymphony.xwork2.interceptor.Interceptor"; + } catch (Exception e) { + cause = e; + message = "Caught Exception while registering Interceptor class " + interceptorClassName; + } catch (NoClassDefFoundError e) { + cause = e; + message = "Could not load class " + interceptorClassName + ". Perhaps it exists but certain dependencies are not available?"; + } + + throw new ConfigurationException(message, cause, interceptorConfig); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/DefaultResultFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/DefaultResultFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultResultFactory.java new file mode 100644 index 0000000..a5a6c79 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultResultFactory.java @@ -0,0 +1,54 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.Result; +import com.opensymphony.xwork2.config.entities.ResultConfig; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.reflection.ReflectionException; +import com.opensymphony.xwork2.util.reflection.ReflectionExceptionHandler; +import com.opensymphony.xwork2.util.reflection.ReflectionProvider; + +import java.util.Map; + +/** + * Default implementation + */ +public class DefaultResultFactory implements ResultFactory { + + private ObjectFactory objectFactory; + private ReflectionProvider reflectionProvider; + + @Inject + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + @Inject + public void setReflectionProvider(ReflectionProvider reflectionProvider) { + this.reflectionProvider = reflectionProvider; + } + + public Result buildResult(ResultConfig resultConfig, Map<String, Object> extraContext) throws Exception { + String resultClassName = resultConfig.getClassName(); + Result result = null; + + if (resultClassName != null) { + result = (Result) objectFactory.buildBean(resultClassName, extraContext); + Map<String, String> params = resultConfig.getParams(); + if (params != null) { + for (Map.Entry<String, String> paramEntry : params.entrySet()) { + try { + reflectionProvider.setProperty(paramEntry.getKey(), paramEntry.getValue(), result, extraContext, true); + } catch (ReflectionException ex) { + if (result instanceof ReflectionExceptionHandler) { + ((ReflectionExceptionHandler) result).handle(ex); + } + } + } + } + } + + return result; + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/DefaultUnknownHandlerFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/DefaultUnknownHandlerFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultUnknownHandlerFactory.java new file mode 100644 index 0000000..58fb339 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultUnknownHandlerFactory.java @@ -0,0 +1,25 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.UnknownHandler; +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.Inject; + +import java.util.Map; + +/** + * Default implementation + */ +public class DefaultUnknownHandlerFactory implements UnknownHandlerFactory { + + private Container container; + + @Inject + public void setContainer(Container container) { + this.container = container; + } + + public UnknownHandler buildUnknownHandler(String unknownHandlerName, Map<String, Object> extraContext) throws Exception { + return container.getInstance(UnknownHandler.class, unknownHandlerName); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/DefaultValidatorFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/DefaultValidatorFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultValidatorFactory.java new file mode 100644 index 0000000..8ce8b77 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/DefaultValidatorFactory.java @@ -0,0 +1,34 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.reflection.ReflectionProvider; +import com.opensymphony.xwork2.validator.Validator; + +import java.util.Map; + +/** + * Default implementation + */ +public class DefaultValidatorFactory implements ValidatorFactory { + + private ObjectFactory objectFactory; + private ReflectionProvider reflectionProvider; + + @Inject + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + @Inject + public void setReflectionProvider(ReflectionProvider reflectionProvider) { + this.reflectionProvider = reflectionProvider; + } + + public Validator buildValidator(String className, Map<String, Object> params, Map<String, Object> extraContext) throws Exception { + Validator validator = (Validator) objectFactory.buildBean(className, extraContext); + reflectionProvider.setProperties(params, validator, extraContext); + + return validator; + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/InterceptorFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/InterceptorFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/InterceptorFactory.java new file mode 100644 index 0000000..742e7ab --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/InterceptorFactory.java @@ -0,0 +1,28 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.config.ConfigurationException; +import com.opensymphony.xwork2.config.entities.InterceptorConfig; +import com.opensymphony.xwork2.interceptor.Interceptor; + +import java.util.Map; + +/** + * Dedicated interface used by {@link com.opensymphony.xwork2.ObjectFactory} to build {@link com.opensymphony.xwork2.interceptor.Interceptor} + */ +public interface InterceptorFactory { + + /** + * Builds an Interceptor from the InterceptorConfig and the Map of + * parameters from the interceptor reference. Implementations of this method + * should ensure that the Interceptor is parameterized with both the + * parameters from the Interceptor config and the interceptor ref Map (the + * interceptor ref params take precedence), and that the Interceptor.init() + * method is called on the Interceptor instance before it is returned. + * + * @param interceptorConfig the InterceptorConfig from the configuration + * @param interceptorRefParams a Map of params provided in the Interceptor reference in the + * Action mapping or InterceptorStack definition + */ + Interceptor buildInterceptor(InterceptorConfig interceptorConfig, Map<String, String> interceptorRefParams) throws ConfigurationException; + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/ResultFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/ResultFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/ResultFactory.java new file mode 100644 index 0000000..c7191cb --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/ResultFactory.java @@ -0,0 +1,15 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.Result; +import com.opensymphony.xwork2.config.entities.ResultConfig; + +import java.util.Map; + +/** + * Used by {@link com.opensymphony.xwork2.ObjectFactory} to build {@link com.opensymphony.xwork2.Result} + */ +public interface ResultFactory { + + Result buildResult(ResultConfig resultConfig, Map<String, Object> extraContext) throws Exception; + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/UnknownHandlerFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/UnknownHandlerFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/UnknownHandlerFactory.java new file mode 100644 index 0000000..244f3ab --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/UnknownHandlerFactory.java @@ -0,0 +1,21 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.UnknownHandler; + +import java.util.Map; + +/** + * Dedicated interface used by {@link com.opensymphony.xwork2.ObjectFactory} to build {@link com.opensymphony.xwork2.UnknownHandler} + */ +public interface UnknownHandlerFactory { + + /** + * Builds unknown handler of given name + * + * @param unknownHandlerName name of unknown handler defined in struts.xml + * @param extraContext extra params + * @return instance of {@link com.opensymphony.xwork2.UnknownHandler} with injected dependencies + */ + UnknownHandler buildUnknownHandler(String unknownHandlerName, Map<String, Object> extraContext) throws Exception; + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/factory/ValidatorFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/factory/ValidatorFactory.java b/core/src/main/java/com/opensymphony/xwork2/factory/ValidatorFactory.java new file mode 100644 index 0000000..bbaa36e --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/factory/ValidatorFactory.java @@ -0,0 +1,21 @@ +package com.opensymphony.xwork2.factory; + +import com.opensymphony.xwork2.validator.Validator; + +import java.util.Map; + +/** + * Dedicated interface used by {@link com.opensymphony.xwork2.ObjectFactory} to build {@link Validator} + */ +public interface ValidatorFactory { + + /** + * Build a Validator of the given type and set the parameters on it + * + * @param className the type of Validator to build + * @param params property name -> value Map to set onto the Validator instance + * @param extraContext a Map of extra context which uses the same keys as the {@link com.opensymphony.xwork2.ActionContext} + */ + Validator buildValidator(String className, Map<String, Object> params, Map<String, Object> extraContext) throws Exception; + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/ConstructionContext.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/ConstructionContext.java b/core/src/main/java/com/opensymphony/xwork2/inject/ConstructionContext.java new file mode 100644 index 0000000..d75d464 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/inject/ConstructionContext.java @@ -0,0 +1,119 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * 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. + */ + +package com.opensymphony.xwork2.inject; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +/** + * Context of a dependency construction. Used to manage circular references. + * + * @author [email protected] (Bob Lee) + */ +class ConstructionContext<T> { + + T currentReference; + boolean constructing; + + List<DelegatingInvocationHandler<T>> invocationHandlers; + + T getCurrentReference() { + return currentReference; + } + + void removeCurrentReference() { + this.currentReference = null; + } + + void setCurrentReference(T currentReference) { + this.currentReference = currentReference; + } + + boolean isConstructing() { + return constructing; + } + + void startConstruction() { + this.constructing = true; + } + + void finishConstruction() { + this.constructing = false; + invocationHandlers = null; + } + + Object createProxy(Class<? super T> expectedType) { + // TODO: if I create a proxy which implements all the interfaces of + // the implementation type, I'll be able to get away with one proxy + // instance (as opposed to one per caller). + + if (!expectedType.isInterface()) { + throw new DependencyException(expectedType.getName() + " is not an interface."); + } + + if (invocationHandlers == null) { + invocationHandlers = new ArrayList<DelegatingInvocationHandler<T>>(); + } + + DelegatingInvocationHandler<T> invocationHandler = new DelegatingInvocationHandler<>(); + invocationHandlers.add(invocationHandler); + + return Proxy.newProxyInstance( + expectedType.getClassLoader(), + new Class[] { expectedType }, + invocationHandler + ); + } + + void setProxyDelegates(T delegate) { + if (invocationHandlers != null) { + for (DelegatingInvocationHandler<T> invocationHandler : invocationHandlers) { + invocationHandler.setDelegate(delegate); + } + } + } + + static class DelegatingInvocationHandler<T> implements InvocationHandler { + + T delegate; + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if (delegate == null) { + throw new IllegalStateException( + "Not finished constructing. Please don't call methods on this" + + " object until the caller's construction is complete."); + } + + try { + return method.invoke(delegate, args); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + + void setDelegate(T delegate) { + this.delegate = delegate; + } + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/Container.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/Container.java b/core/src/main/java/com/opensymphony/xwork2/inject/Container.java new file mode 100644 index 0000000..bca6032 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/inject/Container.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * 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. + */ + +package com.opensymphony.xwork2.inject; + +import java.io.Serializable; +import java.util.Set; + +/** + * Injects dependencies into constructors, methods and fields annotated with + * {@link Inject}. Immutable. + * + * <p>When injecting a method or constructor, you can additionally annotate + * its parameters with {@link Inject} and specify a dependency name. When a + * parameter has no annotation, the container uses the name from the method or + * constructor's {@link Inject} annotation respectively. + * + * <p>For example: + * + * <pre> + * class Foo { + * + * // Inject the int constant named "i". + * @Inject("i") int i; + * + * // Inject the default implementation of Bar and the String constant + * // named "s". + * @Inject Foo(Bar bar, @Inject("s") String s) { + * ... + * } + * + * // Inject the default implementation of Baz and the Bob implementation + * // named "foo". + * @Inject void initialize(Baz baz, @Inject("foo") Bob bob) { + * ... + * } + * + * // Inject the default implementation of Tee. + * @Inject void setTee(Tee tee) { + * ... + * } + * } + * </pre> + * + * <p>To create and inject an instance of {@code Foo}: + * + * <pre> + * Container c = ...; + * Foo foo = c.inject(Foo.class); + * </pre> + * + * @see ContainerBuilder + * @author [email protected] (Bob Lee) + */ +public interface Container extends Serializable { + + /** + * Default dependency name. + */ + String DEFAULT_NAME = "default"; + + /** + * Injects dependencies into the fields and methods of an existing object. + */ + void inject(Object o); + + /** + * Creates and injects a new instance of type {@code implementation}. + */ + <T> T inject(Class<T> implementation); + + /** + * Gets an instance of the given dependency which was declared in + * {@link com.opensymphony.xwork2.inject.ContainerBuilder}. + */ + <T> T getInstance(Class<T> type, String name); + + /** + * Convenience method. Equivalent to {@code getInstance(type, + * DEFAULT_NAME)}. + */ + <T> T getInstance(Class<T> type); + + /** + * Gets a set of all registered names for the given type + * @param type The instance type + * @return A set of registered names or empty set if no instances are registered for that type + */ + Set<String> getInstanceNames(Class<?> type); + + /** + * Sets the scope strategy for the current thread. + */ + void setScopeStrategy(Scope.Strategy scopeStrategy); + + /** + * Removes the scope strategy for the current thread. + */ + void removeScopeStrategy(); +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/ContainerBuilder.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/ContainerBuilder.java b/core/src/main/java/com/opensymphony/xwork2/inject/ContainerBuilder.java new file mode 100644 index 0000000..54f5be6 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/inject/ContainerBuilder.java @@ -0,0 +1,510 @@ +/** + * Copyright (C) 2006 Google Inc. + * <p/> + * 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 + * <p/> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p/> + * 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. + */ + +package com.opensymphony.xwork2.inject; + +import java.lang.reflect.Member; +import java.util.*; +import java.util.logging.Logger; + +/** + * Builds a dependency injection {@link Container}. The combination of + * dependency type and name uniquely identifies a dependency mapping; you can + * use the same name for two different types. Not safe for concurrent use. + * + * <p>Adds the following factories by default: + * + * <ul> + * <li>Injects the current {@link Container}. + * <li>Injects the {@link Logger} for the injected member's declaring class. + * </ul> + * + * @author [email protected] (Bob Lee) + */ +public final class ContainerBuilder { + + final Map<Key<?>, InternalFactory<?>> factories = new HashMap<>(); + final List<InternalFactory<?>> singletonFactories = new ArrayList<>(); + final List<Class<?>> staticInjections = new ArrayList<>(); + boolean created; + boolean allowDuplicates = false; + + private static final InternalFactory<Container> CONTAINER_FACTORY = + new InternalFactory<Container>() { + public Container create(InternalContext context) { + return context.getContainer(); + } + }; + + private static final InternalFactory<Logger> LOGGER_FACTORY = + new InternalFactory<Logger>() { + public Logger create(InternalContext context) { + Member member = context.getExternalContext().getMember(); + return member == null ? Logger.getAnonymousLogger() + : Logger.getLogger(member.getDeclaringClass().getName()); + } + }; + + /** + * Constructs a new builder. + */ + public ContainerBuilder() { + // In the current container as the default Container implementation. + factories.put(Key.newInstance(Container.class, Container.DEFAULT_NAME), CONTAINER_FACTORY); + + // Inject the logger for the injected member's declaring class. + factories.put(Key.newInstance(Logger.class, Container.DEFAULT_NAME), LOGGER_FACTORY); + } + + /** + * Maps a dependency. All methods in this class ultimately funnel through + * here. + */ + private <T> ContainerBuilder factory(final Key<T> key, + InternalFactory<? extends T> factory, Scope scope) { + ensureNotCreated(); + checkKey(key); + final InternalFactory<? extends T> scopedFactory = scope.scopeFactory(key.getType(), key.getName(), factory); + factories.put(key, scopedFactory); + if (scope == Scope.SINGLETON) { + singletonFactories.add(new InternalFactory<T>() { + public T create(InternalContext context) { + try { + context.setExternalContext(ExternalContext.newInstance(null, key, context.getContainerImpl())); + return scopedFactory.create(context); + } finally { + context.setExternalContext(null); + } + } + }); + } + return this; + } + + /** + * Ensures a key isn't already mapped. + */ + private void checkKey(Key<?> key) { + if (factories.containsKey(key) && !allowDuplicates) { + throw new DependencyException("Dependency mapping for " + key + " already exists."); + } + } + + /** + * Maps a factory to a given dependency type and name. + * + * @param type of dependency + * @param name of dependency + * @param factory creates objects to inject + * @param scope scope of injected instances + * @return this builder + */ + public <T> ContainerBuilder factory(final Class<T> type, final String name, + final Factory<? extends T> factory, Scope scope) { + InternalFactory<T> internalFactory = new InternalFactory<T>() { + + public T create(InternalContext context) { + try { + Context externalContext = context.getExternalContext(); + return factory.create(externalContext); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return new LinkedHashMap<String, Object>() {{ + put("type", type); + put("name", name); + put("factory", factory); + }}.toString(); + } + }; + + return factory(Key.newInstance(type, name), internalFactory, scope); + } + + /** + * Convenience method. Equivalent to {@code factory(type, + * Container.DEFAULT_NAME, factory, scope)}. + * + * @see #factory(Class, String, Factory, Scope) + */ + public <T> ContainerBuilder factory(Class<T> type, Factory<? extends T> factory, Scope scope) { + return factory(type, Container.DEFAULT_NAME, factory, scope); + } + + /** + * Convenience method. Equivalent to {@code factory(type, name, factory, + * Scope.DEFAULT)}. + * + * @see #factory(Class, String, Factory, Scope) + */ + public <T> ContainerBuilder factory(Class<T> type, String name, Factory<? extends T> factory) { + return factory(type, name, factory, Scope.DEFAULT); + } + + /** + * Convenience method. Equivalent to {@code factory(type, + * Container.DEFAULT_NAME, factory, Scope.DEFAULT)}. + * + * @see #factory(Class, String, Factory, Scope) + */ + public <T> ContainerBuilder factory(Class<T> type, Factory<? extends T> factory) { + return factory(type, Container.DEFAULT_NAME, factory, Scope.DEFAULT); + } + + /** + * Maps an implementation class to a given dependency type and name. Creates + * instances using the container, recursively injecting dependencies. + * + * @param type of dependency + * @param name of dependency + * @param implementation class + * @param scope scope of injected instances + * @return this builder + */ + public <T> ContainerBuilder factory(final Class<T> type, final String name, + final Class<? extends T> implementation, final Scope scope) { + // This factory creates new instances of the given implementation. + // We have to lazy load the constructor because the Container + // hasn't been created yet. + InternalFactory<? extends T> factory = new InternalFactory<T>() { + + volatile ContainerImpl.ConstructorInjector<? extends T> constructor; + + @SuppressWarnings("unchecked") + public T create(InternalContext context) { + if (constructor == null) { + this.constructor = + context.getContainerImpl().getConstructor(implementation); + } + return (T) constructor.construct(context, type); + } + + @Override + public String toString() { + return new LinkedHashMap<String, Object>() {{ + put("type", type); + put("name", name); + put("implementation", implementation); + put("scope", scope); + }}.toString(); + } + }; + + return factory(Key.newInstance(type, name), factory, scope); + } + + /** + * Maps an implementation class to a given dependency type and name. Creates + * instances using the container, recursively injecting dependencies. + * <p/> + * <p>Sets scope to value from {@link Scoped} annotation on the + * implementation class. Defaults to {@link Scope#DEFAULT} if no annotation + * is found. + * + * @param type of dependency + * @param name of dependency + * @param implementation class + * @return this builder + */ + public <T> ContainerBuilder factory(final Class<T> type, String name, + final Class<? extends T> implementation) { + Scoped scoped = implementation.getAnnotation(Scoped.class); + Scope scope = scoped == null ? Scope.DEFAULT : scoped.value(); + return factory(type, name, implementation, scope); + } + + /** + * Convenience method. Equivalent to {@code factory(type, + * Container.DEFAULT_NAME, implementation)}. + * + * @see #factory(Class, String, Class) + */ + public <T> ContainerBuilder factory(Class<T> type, Class<? extends T> implementation) { + return factory(type, Container.DEFAULT_NAME, implementation); + } + + /** + * Convenience method. Equivalent to {@code factory(type, + * Container.DEFAULT_NAME, type)}. + * + * @see #factory(Class, String, Class) + */ + public <T> ContainerBuilder factory(Class<T> type) { + return factory(type, Container.DEFAULT_NAME, type); + } + + /** + * Convenience method. Equivalent to {@code factory(type, name, type)}. + * + * @see #factory(Class, String, Class) + */ + public <T> ContainerBuilder factory(Class<T> type, String name) { + return factory(type, name, type); + } + + /** + * Convenience method. Equivalent to {@code factory(type, + * Container.DEFAULT_NAME, implementation, scope)}. + * + * @see #factory(Class, String, Class, Scope) + */ + public <T> ContainerBuilder factory(Class<T> type, Class<? extends T> implementation, Scope scope) { + return factory(type, Container.DEFAULT_NAME, implementation, scope); + } + + /** + * Convenience method. Equivalent to {@code factory(type, + * Container.DEFAULT_NAME, type, scope)}. + * + * @see #factory(Class, String, Class, Scope) + */ + public <T> ContainerBuilder factory(Class<T> type, Scope scope) { + return factory(type, Container.DEFAULT_NAME, type, scope); + } + + /** + * Convenience method. Equivalent to {@code factory(type, name, type, + * scope)}. + * + * @see #factory(Class, String, Class, Scope) + */ + public <T> ContainerBuilder factory(Class<T> type, String name, Scope scope) { + return factory(type, name, type, scope); + } + + /** + * Convenience method. Equivalent to {@code alias(type, Container.DEFAULT_NAME, + * type)}. + * + * @see #alias(Class, String, String) + */ + public <T> ContainerBuilder alias(Class<T> type, String alias) { + return alias(type, Container.DEFAULT_NAME, alias); + } + + /** + * Maps an existing factory to a new name. + * + * @param type of dependency + * @param name of dependency + * @param alias of to the dependency + * @return this builder + */ + public <T> ContainerBuilder alias(Class<T> type, String name, String alias) { + return alias(Key.newInstance(type, name), Key.newInstance(type, alias)); + } + + /** + * Maps an existing dependency. All methods in this class ultimately funnel through + * here. + */ + private <T> ContainerBuilder alias(final Key<T> key, + final Key<T> aliasKey) { + ensureNotCreated(); + checkKey(aliasKey); + + final InternalFactory<? extends T> scopedFactory = (InternalFactory<? extends T>) factories.get(key); + if (scopedFactory == null) { + throw new DependencyException("Dependency mapping for " + key + " doesn't exists."); + } + factories.put(aliasKey, scopedFactory); + return this; + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, String value) { + return constant(String.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, int value) { + return constant(int.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, long value) { + return constant(long.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, boolean value) { + return constant(boolean.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, double value) { + return constant(double.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, float value) { + return constant(float.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, short value) { + return constant(short.class, name, value); + } + + /** + * Maps a constant value to the given name. + */ + public ContainerBuilder constant(String name, char value) { + return constant(char.class, name, value); + } + + /** + * Maps a class to the given name. + */ + public ContainerBuilder constant(String name, Class value) { + return constant(Class.class, name, value); + } + + /** + * Maps an enum to the given name. + */ + public <E extends Enum<E>> ContainerBuilder constant(String name, E value) { + return constant(value.getDeclaringClass(), name, value); + } + + /** + * Maps a constant value to the given type and name. + */ + private <T> ContainerBuilder constant(final Class<T> type, final String name, final T value) { + InternalFactory<T> factory = new InternalFactory<T>() { + public T create(InternalContext ignored) { + return value; + } + + @Override + public String toString() { + return new LinkedHashMap<String, Object>() { + { + put("type", type); + put("name", name); + put("value", value); + } + }.toString(); + } + }; + + return factory(Key.newInstance(type, name), factory, Scope.DEFAULT); + } + + /** + * Upon creation, the {@link Container} will inject static fields and methods + * into the given classes. + * + * @param types for which static members will be injected + */ + public ContainerBuilder injectStatics(Class<?>... types) { + staticInjections.addAll(Arrays.asList(types)); + return this; + } + + /** + * Returns true if this builder contains a mapping for the given type and + * name. + */ + public boolean contains(Class<?> type, String name) { + return factories.containsKey(Key.newInstance(type, name)); + } + + /** + * Convenience method. Equivalent to {@code contains(type, + * Container.DEFAULT_NAME)}. + */ + public boolean contains(Class<?> type) { + return contains(type, Container.DEFAULT_NAME); + } + + /** + * Creates a {@link Container} instance. Injects static members for classes + * which were registered using {@link #injectStatics(Class...)}. + * + * @param loadSingletons If true, the container will load all singletons + * now. If false, the container will lazily load singletons. Eager loading + * is appropriate for production use while lazy loading can speed + * development. + * @throws IllegalStateException if called more than once + */ + public Container create(boolean loadSingletons) { + ensureNotCreated(); + created = true; + final ContainerImpl container = new ContainerImpl(new HashMap<>(factories)); + if (loadSingletons) { + container.callInContext(new ContainerImpl.ContextualCallable<Void>() { + public Void call(InternalContext context) { + for (InternalFactory<?> factory : singletonFactories) { + factory.create(context); + } + return null; + } + }); + } + container.injectStatics(staticInjections); + return container; + } + + /** + * Currently we only support creating one Container instance per builder. + * If we want to support creating more than one container per builder, + * we should move to a "factory factory" model where we create a factory + * instance per Container. Right now, one factory instance would be + * shared across all the containers, singletons synchronize on the + * container when lazy loading, etc. + */ + private void ensureNotCreated() { + if (created) { + throw new IllegalStateException("Container already created."); + } + } + + public void setAllowDuplicates(boolean val) { + allowDuplicates = val; + } + + /** + * Implemented by classes which participate in building a container. + */ + public interface Command { + + /** + * Contributes factories to the given builder. + * + * @param builder + */ + void build(ContainerBuilder builder); + } +}
