Adds Java8 specific ClassFinder with injection
Project: http://git-wip-us.apache.org/repos/asf/struts/repo Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/463a90e8 Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/463a90e8 Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/463a90e8 Branch: refs/heads/master Commit: 463a90e8b7ce2166ac3a6e25d82fa7e2378cb18c Parents: a7cf429 Author: Lukasz Lenart <lukaszlen...@apache.org> Authored: Sat Dec 20 21:49:36 2014 +0100 Committer: Lukasz Lenart <lukaszlen...@apache.org> Committed: Sat Dec 20 21:49:36 2014 +0100 ---------------------------------------------------------------------- .../struts2/convention/Java8ClassFinder.java | 597 +++++++++++++++++++ .../convention/Java8ClassFinderFactory.java | 48 ++ .../src/main/resources/struts-plugin.xml | 33 + 3 files changed, 678 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/struts/blob/463a90e8/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinder.java ---------------------------------------------------------------------- diff --git a/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinder.java b/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinder.java new file mode 100644 index 0000000..21b772c --- /dev/null +++ b/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinder.java @@ -0,0 +1,597 @@ +/* + * Copyright 2002-2003,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 org.apache.struts2.convention; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.FileManager; +import com.opensymphony.xwork2.FileManagerFactory; +import com.opensymphony.xwork2.XWorkException; +import com.opensymphony.xwork2.util.finder.ClassFinder; +import com.opensymphony.xwork2.util.finder.ClassLoaderInterface; +import com.opensymphony.xwork2.util.finder.Test; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; +import org.apache.commons.lang3.StringUtils; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +/** + * Copy of {@link com.opensymphony.xwork2.util.finder.DefaultClassFinder} with proper support for Java8 + */ +public class Java8ClassFinder implements ClassFinder { + + private static final Logger LOG = LoggerFactory.getLogger(Java8ClassFinder.class); + + private final Map<String, List<Info>> annotated = new HashMap<String, List<Info>>(); + private final Map<String, ClassInfo> classInfos = new LinkedHashMap<String, ClassInfo>(); + + private final List<String> classesNotLoaded = new ArrayList<String>(); + + private boolean extractBaseInterfaces; + private ClassLoaderInterface classLoaderInterface; + private FileManager fileManager; + + public Java8ClassFinder(ClassLoaderInterface classLoaderInterface, Collection<URL> urls, boolean extractBaseInterfaces, Set<String> protocols, Test<String> classNameFilter) { + this.classLoaderInterface = classLoaderInterface; + this.extractBaseInterfaces = extractBaseInterfaces; + this.fileManager = ActionContext.getContext().getInstance(FileManagerFactory.class).getFileManager(); + + List<String> classNames = new ArrayList<String>(); + for (URL location : urls) { + try { + if (protocols.contains(location.getProtocol())) { + classNames.addAll(jar(location)); + } else if ("file".equals(location.getProtocol())) { + try { + // See if it's actually a jar + URL jarUrl = new URL("jar", "", location.toExternalForm() + "!/"); + JarURLConnection juc = (JarURLConnection) jarUrl.openConnection(); + juc.getJarFile(); + classNames.addAll(jar(jarUrl)); + } catch (IOException e) { + classNames.addAll(file(location)); + } + } + } catch (Exception e) { + if (LOG.isErrorEnabled()) + LOG.error("Unable to read URL [#0]", e, location.toExternalForm()); + } + } + + for (String className : classNames) { + try { + if (classNameFilter.test(className)) + readClassDef(className); + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Unable to read class [#0]", e, className); + } + } + } + + public Java8ClassFinder(Class... classes){ + this(Arrays.asList(classes)); + } + + public Java8ClassFinder(List<Class> classes){ + this.classLoaderInterface = null; + List<Info> infos = new ArrayList<Info>(); + List<Package> packages = new ArrayList<Package>(); + for (Class clazz : classes) { + + Package aPackage = clazz.getPackage(); + if (aPackage != null && !packages.contains(aPackage)){ + infos.add(new PackageInfo(aPackage)); + packages.add(aPackage); + } + + ClassInfo classInfo = new ClassInfo(clazz, this); + infos.add(classInfo); + classInfos.put(classInfo.getName(), classInfo); + for (Method method : clazz.getDeclaredMethods()) { + infos.add(new MethodInfo(classInfo, method)); + } + + for (Constructor constructor : clazz.getConstructors()) { + infos.add(new MethodInfo(classInfo, constructor)); + } + + for (Field field : clazz.getDeclaredFields()) { + infos.add(new FieldInfo(classInfo, field)); + } + } + + for (Info info : infos) { + for (AnnotationInfo annotation : info.getAnnotations()) { + List<Info> annotationInfos = getAnnotationInfos(annotation.getName()); + annotationInfos.add(info); + } + } + } + + public ClassLoaderInterface getClassLoaderInterface() { + return classLoaderInterface; + } + + public boolean isAnnotationPresent(Class<? extends Annotation> annotation) { + List<Info> infos = annotated.get(annotation.getName()); + return infos != null && !infos.isEmpty(); + } + + /** + * Returns a list of classes that could not be loaded in last invoked findAnnotated* method. + * <p/> + * The list will only contain entries of classes whose byte code matched the requirements + * of last invoked find* method, but were unable to be loaded and included in the results. + * <p/> + * The list returned is unmodifiable. Once obtained, the returned list will be a live view of the + * results from the last findAnnotated* method call. + * <p/> + * This method is not thread safe. + * @return an unmodifiable live view of classes that could not be loaded in previous findAnnotated* call. + */ + public List<String> getClassesNotLoaded() { + return Collections.unmodifiableList(classesNotLoaded); + } + + public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) { + classesNotLoaded.clear(); + List<Package> packages = new ArrayList<Package>(); + List<Info> infos = getAnnotationInfos(annotation.getName()); + for (Info info : infos) { + if (info instanceof PackageInfo) { + PackageInfo packageInfo = (PackageInfo) info; + try { + Package pkg = packageInfo.get(); + // double check via proper reflection + if (pkg.isAnnotationPresent(annotation)) { + packages.add(pkg); + } + } catch (ClassNotFoundException e) { + classesNotLoaded.add(packageInfo.getName()); + } + } + } + return packages; + } + + public List<Class> findAnnotatedClasses(Class<? extends Annotation> annotation) { + classesNotLoaded.clear(); + List<Class> classes = new ArrayList<Class>(); + List<Info> infos = getAnnotationInfos(annotation.getName()); + for (Info info : infos) { + if (info instanceof ClassInfo) { + ClassInfo classInfo = (ClassInfo) info; + try { + Class clazz = classInfo.get(); + // double check via proper reflection + if (clazz.isAnnotationPresent(annotation)) { + classes.add(clazz); + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + } + return classes; + } + + public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) { + classesNotLoaded.clear(); + List<ClassInfo> seen = new ArrayList<ClassInfo>(); + List<Method> methods = new ArrayList<Method>(); + List<Info> infos = getAnnotationInfos(annotation.getName()); + for (Info info : infos) { + if (info instanceof MethodInfo && !"<init>".equals(info.getName())) { + MethodInfo methodInfo = (MethodInfo) info; + ClassInfo classInfo = methodInfo.getDeclaringClass(); + + if (seen.contains(classInfo)) continue; + + seen.add(classInfo); + + try { + Class clazz = classInfo.get(); + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(annotation)) { + methods.add(method); + } + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + } + return methods; + } + + public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) { + classesNotLoaded.clear(); + List<ClassInfo> seen = new ArrayList<ClassInfo>(); + List<Constructor> constructors = new ArrayList<Constructor>(); + List<Info> infos = getAnnotationInfos(annotation.getName()); + for (Info info : infos) { + if (info instanceof MethodInfo && "<init>".equals(info.getName())) { + MethodInfo methodInfo = (MethodInfo) info; + ClassInfo classInfo = methodInfo.getDeclaringClass(); + + if (seen.contains(classInfo)) continue; + + seen.add(classInfo); + + try { + Class clazz = classInfo.get(); + for (Constructor constructor : clazz.getConstructors()) { + if (constructor.isAnnotationPresent(annotation)) { + constructors.add(constructor); + } + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + } + return constructors; + } + + public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) { + classesNotLoaded.clear(); + List<ClassInfo> seen = new ArrayList<ClassInfo>(); + List<Field> fields = new ArrayList<Field>(); + List<Info> infos = getAnnotationInfos(annotation.getName()); + for (Info info : infos) { + if (info instanceof FieldInfo) { + FieldInfo fieldInfo = (FieldInfo) info; + ClassInfo classInfo = fieldInfo.getDeclaringClass(); + + if (seen.contains(classInfo)) continue; + + seen.add(classInfo); + + try { + Class clazz = classInfo.get(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(annotation)) { + fields.add(field); + } + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + } + return fields; + } + + public List<Class> findClassesInPackage(String packageName, boolean recursive) { + classesNotLoaded.clear(); + List<Class> classes = new ArrayList<Class>(); + for (ClassInfo classInfo : classInfos.values()) { + try { + if (recursive && classInfo.getPackageName().startsWith(packageName)){ + classes.add(classInfo.get()); + } else if (classInfo.getPackageName().equals(packageName)){ + classes.add(classInfo.get()); + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + return classes; + } + + public List<Class> findClasses(Test<ClassInfo> test) { + classesNotLoaded.clear(); + List<Class> classes = new ArrayList<Class>(); + for (ClassInfo classInfo : classInfos.values()) { + try { + if (test.test(classInfo)) { + classes.add(classInfo.get()); + } + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + return classes; + } + + public List<Class> findClasses() { + classesNotLoaded.clear(); + List<Class> classes = new ArrayList<Class>(); + for (ClassInfo classInfo : classInfos.values()) { + try { + classes.add(classInfo.get()); + } catch (Throwable e) { + if (LOG.isErrorEnabled()) + LOG.error("Error loading class [#0]", e, classInfo.getName()); + classesNotLoaded.add(classInfo.getName()); + } + } + return classes; + } + + private static List<URL> getURLs(ClassLoaderInterface classLoader, String[] dirNames) { + List<URL> urls = new ArrayList<URL>(); + for (String dirName : dirNames) { + try { + Enumeration<URL> classLoaderURLs = classLoader.getResources(dirName); + while (classLoaderURLs.hasMoreElements()) { + URL url = classLoaderURLs.nextElement(); + urls.add(url); + } + } catch (IOException ioe) { + if (LOG.isErrorEnabled()) + LOG.error("Could not read driectory [#0]", ioe, dirName); + } + } + + return urls; + } + + private List<String> file(URL location) { + List<String> classNames = new ArrayList<String>(); + File dir = new File(URLDecoder.decode(location.getPath())); + if ("META-INF".equals(dir.getName())) { + dir = dir.getParentFile(); // Scrape "META-INF" off + } + if (dir.isDirectory()) { + scanDir(dir, classNames, ""); + } + return classNames; + } + + private void scanDir(File dir, List<String> classNames, String packageName) { + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + scanDir(file, classNames, packageName + file.getName() + "."); + } else if (file.getName().endsWith(".class")) { + String name = file.getName(); + name = name.replaceFirst(".class$", ""); + // Classes packaged in an exploded .war (e.g. in a VFS file system) should not + // have WEB-INF.classes in their package name. + classNames.add(StringUtils.removeStart(packageName, "WEB-INF.classes.") + name); + } + } + } + + private List<String> jar(URL location) throws IOException { + URL url = fileManager.normalizeToFileProtocol(location); + if (url != null) { + InputStream in = url.openStream(); + try { + JarInputStream jarStream = new JarInputStream(in); + return jar(jarStream); + } finally { + in.close(); + } + } else if (LOG.isDebugEnabled()) + LOG.debug("Unable to read [#0]", location.toExternalForm()); + + return Collections.emptyList(); + } + + private List<String> jar(JarInputStream jarStream) throws IOException { + List<String> classNames = new ArrayList<String>(); + + JarEntry entry; + while ((entry = jarStream.getNextJarEntry()) != null) { + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + String className = entry.getName(); + className = className.replaceFirst(".class$", ""); + + //war files are treated as .jar files, so takeout WEB-INF/classes + className = StringUtils.removeStart(className, "WEB-INF/classes/"); + + className = className.replace('/', '.'); + classNames.add(className); + } + + return classNames; + } + + private List<Info> getAnnotationInfos(String name) { + List<Info> infos = annotated.get(name); + if (infos == null) { + infos = new ArrayList<Info>(); + annotated.put(name, infos); + } + return infos; + } + + private void readClassDef(String className) { + if (!className.endsWith(".class")) { + className = className.replace('.', '/') + ".class"; + } + try { + URL resource = classLoaderInterface.getResource(className); + if (resource != null) { + InputStream in = resource.openStream(); + try { + ClassReader classReader = new ClassReader(in); + classReader.accept(new InfoBuildingClassVisitor(this), ClassReader.SKIP_DEBUG); + } finally { + in.close(); + } + } else { + throw new XWorkException("Could not load " + className); + } + } catch (IOException e) { + throw new XWorkException("Could not load " + className, e); + } + + } + + public class InfoBuildingClassVisitor extends ClassVisitor { + private Info info; + private ClassFinder classFinder; + + public InfoBuildingClassVisitor(ClassFinder classFinder) { + super(Opcodes.ASM5); + this.classFinder = classFinder; + } + + public InfoBuildingClassVisitor(Info info, ClassFinder classFinder) { + this(classFinder); + this.info = info; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (name.endsWith("package-info")) { + info = new PackageInfo(javaName(name), classFinder); + } else { + ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName), classFinder); + + for (String interfce : interfaces) { + classInfo.getInterfaces().add(javaName(interfce)); + } + info = classInfo; + classInfos.put(classInfo.getName(), classInfo); + + if (extractBaseInterfaces) + extractSuperInterfaces(classInfo); + } + } + + private void extractSuperInterfaces(ClassInfo classInfo) { + String superType = classInfo.getSuperType(); + + if (superType != null) { + ClassInfo base = classInfos.get(superType); + + if (base == null) { + //try to load base + String resource = superType.replace('.', '/') + ".class"; + readClassDef(resource); + base = classInfos.get(superType); + } + + if (base != null) { + List<String> interfaces = classInfo.getSuperInterfaces(); + interfaces.addAll(base.getSuperInterfaces()); + interfaces.addAll(base.getInterfaces()); + } + } + } + + private String javaName(String name) { + return (name == null)? null:name.replace('/', '.'); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + AnnotationInfo annotationInfo = new AnnotationInfo(desc); + info.getAnnotations().add(annotationInfo); + getAnnotationInfos(annotationInfo.getName()).add(info); + return null; + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + ClassInfo classInfo = ((ClassInfo) info); + FieldInfo fieldInfo = new FieldInfo(classInfo, name, desc); + classInfo.getFields().add(fieldInfo); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + ClassInfo classInfo = ((ClassInfo) info); + MethodInfo methodInfo = new MethodInfo(classInfo, name, desc); + classInfo.getMethods().add(methodInfo); + return new InfoBuildingMethodVisitor(methodInfo); + } + } + + public class InfoBuildingMethodVisitor extends MethodVisitor { + private Info info; + + public InfoBuildingMethodVisitor() { + super(Opcodes.ASM5); + } + + public InfoBuildingMethodVisitor(Info info) { + this(); + this.info = info; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + AnnotationInfo annotationInfo = new AnnotationInfo(desc); + info.getAnnotations().add(annotationInfo); + getAnnotationInfos(annotationInfo.getName()).add(info); + return null; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int param, String desc, boolean visible) { + MethodInfo methodInfo = ((MethodInfo) info); + List<AnnotationInfo> annotationInfos = methodInfo.getParameterAnnotations(param); + AnnotationInfo annotationInfo = new AnnotationInfo(desc); + annotationInfos.add(annotationInfo); + return null; + } + } + + private static final class DefaultClassnameFilterImpl implements Test<String> { + public boolean test(String className) { + return true; + } + } +} + http://git-wip-us.apache.org/repos/asf/struts/blob/463a90e8/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinderFactory.java ---------------------------------------------------------------------- diff --git a/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinderFactory.java b/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinderFactory.java new file mode 100644 index 0000000..3ab5f9e --- /dev/null +++ b/plugins/java8-support/src/main/java/org/apache/struts2/convention/Java8ClassFinderFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2003,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 org.apache.struts2.convention; + +import com.opensymphony.xwork2.util.finder.ClassFinder; +import com.opensymphony.xwork2.util.finder.ClassFinderFactory; +import com.opensymphony.xwork2.util.finder.ClassLoaderInterface; +import com.opensymphony.xwork2.util.finder.Test; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +import java.net.URL; +import java.util.Collection; +import java.util.Set; + +public class Java8ClassFinderFactory implements ClassFinderFactory { + + private static final Logger LOG = LoggerFactory.getLogger(Java8ClassFinderFactory.class); + + public Java8ClassFinderFactory() { + try { + LOG.trace("Checking if ASM5 is on the classpath...."); + Class.forName("org.objectweb.asm.MethodVisitor"); + LOG.trace("Proper version of ASM5 is in use!"); + } catch (ClassNotFoundException e) { + LOG.warn("ASM5 is missing or older version is used! If you use Maven, please exclude asm.jar and asm-commons.jar version 3 from xwork!"); + } + } + + public ClassFinder buildClassFinder(ClassLoaderInterface classLoaderInterface, Collection<URL> urls, boolean extractBaseInterfaces, Set<String> protocols, Test<String> classNameFilter) { + LOG.debug("Creating new instance of Java8ClassFinder"); + return new Java8ClassFinder(classLoaderInterface, urls, extractBaseInterfaces, protocols, classNameFilter); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/463a90e8/plugins/java8-support/src/main/resources/struts-plugin.xml ---------------------------------------------------------------------- diff --git a/plugins/java8-support/src/main/resources/struts-plugin.xml b/plugins/java8-support/src/main/resources/struts-plugin.xml new file mode 100644 index 0000000..c765cf8 --- /dev/null +++ b/plugins/java8-support/src/main/resources/struts-plugin.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!-- +/* + * $Id$ + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +--> + +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" + "http://struts.apache.org/dtds/struts-2.3.dtd"> + +<struts order="25"> + + <bean type="com.opensymphony.xwork2.util.finder.ClassFinderFactory" class="org.apache.struts2.convention.Java8ClassFinderFactory" scope="singleton"/> + +</struts>