View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.util;
18  
19  import java.lang.reflect.Array;
20  import java.lang.reflect.Constructor;
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Modifier;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.TreeMap;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.commons.vfs2.FileSystemConfigBuilder;
32  import org.apache.commons.vfs2.FileSystemException;
33  import org.apache.commons.vfs2.FileSystemManager;
34  import org.apache.commons.vfs2.FileSystemOptions;
35  
36  /**
37   * This class use reflection to set a configuration value using the fileSystemConfigBuilder associated the a scheme.
38   * <p>
39   * Example:
40   * </p>
41   *
42   * <pre>
43   * FileSystemOptions fso = new FileSystemOptions();
44   * DelegatingFileSystemOptionsBuilder delegate = new DelegatingFileSystemOptionsBuilder(VFS.getManager());
45   * delegate.setConfigString(fso, "sftp", "identities", "c:/tmp/test.ident");
46   * delegate.setConfigString(fso, "http", "proxyPort", "8080");
47   * delegate.setConfigClass(fso, "sftp", "userinfo", TrustEveryoneUserInfo.class);
48   * </pre>
49   */
50  public class DelegatingFileSystemOptionsBuilder {
51  
52      /**
53       * Context.
54       */
55      private static final class Context {
56          private final FileSystemOptions fso;
57          private final String scheme;
58          private final String name;
59          private final Object[] values;
60  
61          private List<Method> configSetters;
62          private FileSystemConfigBuilder fileSystemConfigBuilder;
63  
64          private Context(final FileSystemOptions fso, final String scheme, final String name, final Object[] values) {
65              this.fso = fso;
66              this.scheme = scheme;
67              this.name = name;
68              this.values = values;
69          }
70      }
71      @SuppressWarnings("unchecked") // OK, it is a String
72      private static final Class<String>[] STRING_PARAM = new Class[] {String.class};
73      private static final Map<String, Class<?>> PRIMITIVE_TO_OBJECT = new TreeMap<>();
74  
75      private static final Log log = LogFactory.getLog(DelegatingFileSystemOptionsBuilder.class);
76      static {
77          PRIMITIVE_TO_OBJECT.put(Void.TYPE.getName(), Void.class);
78          PRIMITIVE_TO_OBJECT.put(Boolean.TYPE.getName(), Boolean.class);
79          PRIMITIVE_TO_OBJECT.put(Byte.TYPE.getName(), Byte.class);
80          PRIMITIVE_TO_OBJECT.put(Character.TYPE.getName(), Character.class);
81          PRIMITIVE_TO_OBJECT.put(Short.TYPE.getName(), Short.class);
82          PRIMITIVE_TO_OBJECT.put(Integer.TYPE.getName(), Integer.class);
83          PRIMITIVE_TO_OBJECT.put(Long.TYPE.getName(), Long.class);
84          PRIMITIVE_TO_OBJECT.put(Double.TYPE.getName(), Double.class);
85          PRIMITIVE_TO_OBJECT.put(Float.TYPE.getName(), Float.class);
86      }
87  
88      private final FileSystemManager manager;
89  
90      private final Map<String, Map<String, List<Method>>> beanMethods = new TreeMap<>();
91  
92      /**
93       * Constructs a new instance.
94       * <p>
95       * Pass in your fileSystemManager instance.
96       * </p>
97       *
98       * @param manager the manager to use to get the fileSystemConfigBuilder associated to a scheme
99       */
100     public DelegatingFileSystemOptionsBuilder(final FileSystemManager manager) {
101         this.manager = manager;
102     }
103 
104     /**
105      * Tries to convert the value and pass it to the given method
106      */
107     private boolean convertValuesAndInvoke(final Method configSetter, final Context ctx) throws FileSystemException {
108         final Class<?>[] parameters = configSetter.getParameterTypes();
109         if (parameters.length < 2) {
110             return false;
111         }
112         if (!parameters[0].isAssignableFrom(FileSystemOptions.class)) {
113             return false;
114         }
115 
116         final Class<?> valueParameter = parameters[1];
117         Class<?> type;
118         if (valueParameter.isArray()) {
119             type = valueParameter.getComponentType();
120         } else {
121             if (ctx.values.length > 1) {
122                 return false;
123             }
124 
125             type = valueParameter;
126         }
127 
128         if (type.isPrimitive()) {
129             final Class<?> objectType = PRIMITIVE_TO_OBJECT.get(type.getName());
130             if (objectType == null) {
131                 log.warn(Messages.getString("vfs.provider/config-unexpected-primitive.error", type.getName()));
132                 return false;
133             }
134             type = objectType;
135         }
136 
137         final Class<? extends Object> valueClass = ctx.values[0].getClass();
138         if (type.isAssignableFrom(valueClass)) {
139             // can set value directly
140             invokeSetter(valueParameter, ctx, configSetter, ctx.values);
141             return true;
142         }
143         if (valueClass != String.class) {
144             log.warn(Messages.getString("vfs.provider/config-unexpected-value-class.error", valueClass.getName(), ctx.scheme, ctx.name));
145             return false;
146         }
147 
148         final Object convertedValues = Array.newInstance(type, ctx.values.length);
149 
150         Constructor<?> valueConstructor;
151         try {
152             valueConstructor = type.getConstructor(STRING_PARAM);
153         } catch (final NoSuchMethodException e) {
154             valueConstructor = null;
155         }
156         if (valueConstructor != null) {
157             // can convert using constructor
158             for (int iterValues = 0; iterValues < ctx.values.length; iterValues++) {
159                 try {
160                     Array.set(convertedValues, iterValues, valueConstructor.newInstance(ctx.values[iterValues]));
161                 } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
162                     throw new FileSystemException(e);
163                 }
164             }
165 
166             invokeSetter(valueParameter, ctx, configSetter, convertedValues);
167             return true;
168         }
169 
170         Method valueFactory;
171         try {
172             valueFactory = type.getMethod("valueOf", STRING_PARAM);
173             if (!Modifier.isStatic(valueFactory.getModifiers())) {
174                 valueFactory = null;
175             }
176         } catch (final NoSuchMethodException e) {
177             valueFactory = null;
178         }
179 
180         if (valueFactory != null) {
181             // can convert using factory method (valueOf)
182             for (int iterValues = 0; iterValues < ctx.values.length; iterValues++) {
183                 try {
184                     Array.set(convertedValues, iterValues, valueFactory.invoke(null, ctx.values[iterValues]));
185                 } catch (final IllegalAccessException | InvocationTargetException e) {
186                     throw new FileSystemException(e);
187                 }
188             }
189 
190             invokeSetter(valueParameter, ctx, configSetter, convertedValues);
191             return true;
192         }
193 
194         return false;
195     }
196 
197     /**
198      * Creates the list of all set*() methods for the given scheme
199      */
200     private Map<String, List<Method>> createSchemeMethods(final String scheme) throws FileSystemException {
201         final FileSystemConfigBuilder fscb = getManager().getFileSystemConfigBuilder(scheme);
202         FileSystemException.requireNonNull(fscb, "vfs.provider/no-config-builder.error", scheme);
203 
204         final Map<String, List<Method>> schemeMethods = new TreeMap<>();
205 
206         final Method[] methods = fscb.getClass().getMethods();
207         for (final Method method : methods) {
208             if (!Modifier.isPublic(method.getModifiers())) {
209                 continue;
210             }
211 
212             final String methodName = method.getName();
213             if (!methodName.startsWith("set")) {
214                 // not a setter
215                 continue;
216             }
217 
218             final String key = methodName.substring(3).toLowerCase();
219 
220             final List<Method> configSetter = schemeMethods.computeIfAbsent(key, k -> new ArrayList<>(2));
221             configSetter.add(method);
222         }
223 
224         return schemeMethods;
225     }
226 
227     /**
228      * Fills all available set*() methods for the context-scheme into the context.
229      */
230     private boolean fillConfigSetters(final Context ctx) throws FileSystemException {
231         final Map<String, List<Method>> schemeMethods = getSchemeMethods(ctx.scheme);
232         final List<Method> configSetters = schemeMethods.get(ctx.name.toLowerCase());
233         if (configSetters == null) {
234             return false;
235         }
236 
237         ctx.configSetters = configSetters;
238         return true;
239     }
240 
241     /**
242      * Gets the FileSystemManager.
243      *
244      * @return the FileSystemManager.
245      */
246     protected FileSystemManager getManager() {
247         return manager;
248     }
249 
250     /**
251      * Gets (cached) list of set*() methods for the given scheme
252      */
253     private Map<String, List<Method>> getSchemeMethods(final String scheme) throws FileSystemException {
254         Map<String, List<Method>> schemeMethods = beanMethods.get(scheme);
255         if (schemeMethods == null) {
256             schemeMethods = createSchemeMethods(scheme);
257             beanMethods.put(scheme, schemeMethods);
258         }
259 
260         return schemeMethods;
261     }
262 
263     /**
264      * Invokes the method with the converted values
265      */
266     private void invokeSetter(final Class<?> valueParameter, final Context ctx, final Method configSetter, final Object values) throws FileSystemException {
267         final Object[] args;
268         if (valueParameter.isArray()) {
269             args = new Object[] {ctx.fso, values};
270         } else {
271             args = new Object[] {ctx.fso, Array.get(values, 0)};
272         }
273         try {
274             configSetter.invoke(ctx.fileSystemConfigBuilder, args);
275         } catch (final IllegalAccessException | InvocationTargetException e) {
276             throw new FileSystemException(e);
277         }
278     }
279 
280     /**
281      * Sets a single class value.
282      * <p>
283      * The class has to implement a no-args constructor, else the instantiation might fail.
284      * </p>
285      *
286      * @param fso FileSystemOptions
287      * @param scheme scheme
288      * @param name name
289      * @param className className
290      * @throws FileSystemException if an error occurs.
291      * @throws ReflectiveOperationException if a class cannot be accessed or instantiated.
292      */
293     public void setConfigClass(final FileSystemOptions fso, final String scheme, final String name,
294             final Class<?> className) throws FileSystemException, ReflectiveOperationException {
295         setConfigClasses(fso, scheme, name, new Class[] {className});
296     }
297 
298     /**
299      * Sets an array of class values.
300      * <p>
301      * The class has to implement a no-args constructor, else the instantiation might fail.
302      * </p>
303      *
304      * @param fso FileSystemOptions
305      * @param scheme scheme
306      * @param name name
307      * @param classNames classNames
308      * @throws FileSystemException if an error occurs.
309      * @throws ReflectiveOperationException if a class cannot be accessed or instantiated.
310      */
311     public void setConfigClasses(final FileSystemOptions fso, final String scheme, final String name,
312             final Class<?>[] classNames) throws FileSystemException, ReflectiveOperationException {
313         final Object[] values = new Object[classNames.length];
314         for (int iterClassNames = 0; iterClassNames < values.length; iterClassNames++) {
315             values[iterClassNames] = classNames[iterClassNames].getConstructor().newInstance();
316         }
317 
318         final Context ctx = new Context(fso, scheme, name, values);
319 
320         setValues(ctx);
321     }
322 
323     /**
324      * Sets a single string value.
325      *
326      * @param fso FileSystemOptions
327      * @param scheme scheme
328      * @param name name
329      * @param value value
330      * @throws FileSystemException if an error occurs.
331      */
332     public void setConfigString(final FileSystemOptions fso, final String scheme, final String name, final String value)
333             throws FileSystemException {
334         setConfigStrings(fso, scheme, name, new String[] {value});
335     }
336 
337     /**
338      * Sets an array of string value.
339      *
340      * @param fso FileSystemOptions
341      * @param scheme scheme
342      * @param name name
343      * @param values values
344      * @throws FileSystemException if an error occurs.
345      */
346     public void setConfigStrings(final FileSystemOptions fso, final String scheme, final String name,
347             final String[] values) throws FileSystemException {
348         final Context ctx = new Context(fso, scheme, name, values);
349 
350         setValues(ctx);
351     }
352 
353     /**
354      * Sets the values using the information of the given context.
355      */
356     private void setValues(final Context ctx) throws FileSystemException {
357         // find all setter methods suitable for the given "name"
358         if (!fillConfigSetters(ctx)) {
359             throw new FileSystemException("vfs.provider/config-key-invalid.error", ctx.scheme, ctx.name);
360         }
361 
362         // get the fileSystemConfigBuilder
363         ctx.fileSystemConfigBuilder = getManager().getFileSystemConfigBuilder(ctx.scheme);
364 
365         // try to find a setter which could accept the value
366         for (final Method configSetter : ctx.configSetters) {
367             if (convertValuesAndInvoke(configSetter, ctx)) {
368                 return;
369             }
370         }
371 
372         throw new FileSystemException("vfs.provider/config-value-invalid.error", ctx.scheme, ctx.name, ctx.values);
373     }
374 }