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      @SuppressWarnings("unchecked") // OK, it is a String
53      private static final Class<String>[] STRING_PARAM = new Class[] { String.class };
54      private static final Map<String, Class<?>> PRIMATIVE_TO_OBJECT = new TreeMap<>();
55      private static final Log log = LogFactory.getLog(DelegatingFileSystemOptionsBuilder.class);
56  
57      private final FileSystemManager manager;
58      private final Map<String, Map<String, List<Method>>> beanMethods = new TreeMap<>();
59  
60      static {
61          PRIMATIVE_TO_OBJECT.put(Void.TYPE.getName(), Void.class);
62          PRIMATIVE_TO_OBJECT.put(Boolean.TYPE.getName(), Boolean.class);
63          PRIMATIVE_TO_OBJECT.put(Byte.TYPE.getName(), Byte.class);
64          PRIMATIVE_TO_OBJECT.put(Character.TYPE.getName(), Character.class);
65          PRIMATIVE_TO_OBJECT.put(Short.TYPE.getName(), Short.class);
66          PRIMATIVE_TO_OBJECT.put(Integer.TYPE.getName(), Integer.class);
67          PRIMATIVE_TO_OBJECT.put(Long.TYPE.getName(), Long.class);
68          PRIMATIVE_TO_OBJECT.put(Double.TYPE.getName(), Double.class);
69          PRIMATIVE_TO_OBJECT.put(Float.TYPE.getName(), Float.class);
70      }
71  
72      /**
73       * Context.
74       */
75      private static final class Context {
76          private final FileSystemOptions fso;
77          private final String scheme;
78          private final String name;
79          private final Object[] values;
80  
81          private List<Method> configSetters;
82          private FileSystemConfigBuilder fileSystemConfigBuilder;
83  
84          private Context(final FileSystemOptions fso, final String scheme, final String name, final Object[] values) {
85              this.fso = fso;
86              this.scheme = scheme;
87              this.name = name;
88              this.values = values;
89          }
90      }
91  
92      /**
93       * Constructor.
94       * <p>
95       * Pass in your fileSystemManager instance.
96       * </p>
97       *
98       * @param manager the manager to use to get the fileSystemConfigBuilder assocated to a scheme
99       */
100     public DelegatingFileSystemOptionsBuilder(final FileSystemManager manager) {
101         this.manager = manager;
102     }
103 
104     protected FileSystemManager getManager() {
105         return manager;
106     }
107 
108     /**
109      * Sets a single string value.
110      *
111      * @param fso FileSystemOptions
112      * @param scheme scheme
113      * @param name name
114      * @param value value
115      * @throws FileSystemException if an error occurs.
116      */
117     public void setConfigString(final FileSystemOptions fso, final String scheme, final String name, final String value)
118             throws FileSystemException {
119         setConfigStrings(fso, scheme, name, new String[] { value });
120     }
121 
122     /**
123      * Sets an array of string value.
124      *
125      * @param fso FileSystemOptions
126      * @param scheme scheme
127      * @param name name
128      * @param values values
129      * @throws FileSystemException if an error occurs.
130      */
131     public void setConfigStrings(final FileSystemOptions fso, final String scheme, final String name,
132             final String[] values) throws FileSystemException {
133         final Context ctx = new Context(fso, scheme, name, values);
134 
135         setValues(ctx);
136     }
137 
138     /**
139      * Sets a single class value.
140      * <p>
141      * The class has to implement a no-args constructor, else the instantiation might fail.
142      * </p>
143      *
144      * @param fso FileSystemOptions
145      * @param scheme scheme
146      * @param name name
147      * @param className className
148      * @throws FileSystemException if an error occurs.
149      * @throws IllegalAccessException if a class canoot be accessed.
150      * @throws InstantiationException if a class cannot be instantiated.
151      */
152     public void setConfigClass(final FileSystemOptions fso, final String scheme, final String name,
153             final Class<?> className) throws FileSystemException, IllegalAccessException, InstantiationException {
154         setConfigClasses(fso, scheme, name, new Class[] { className });
155     }
156 
157     /**
158      * Sets an array of class values.
159      * <p>
160      * The class has to implement a no-args constructor, else the instantiation might fail.
161      * </p>
162      *
163      * @param fso FileSystemOptions
164      * @param scheme scheme
165      * @param name name
166      * @param classNames classNames
167      * @throws FileSystemException if an error occurs.
168      * @throws IllegalAccessException if a class canoot be accessed.
169      * @throws InstantiationException if a class cannot be instantiated.
170      */
171     public void setConfigClasses(final FileSystemOptions fso, final String scheme, final String name,
172             final Class<?>[] classNames) throws FileSystemException, IllegalAccessException, InstantiationException {
173         final Object[] values = new Object[classNames.length];
174         for (int iterClassNames = 0; iterClassNames < values.length; iterClassNames++) {
175             values[iterClassNames] = classNames[iterClassNames].newInstance();
176         }
177 
178         final Context ctx = new Context(fso, scheme, name, values);
179 
180         setValues(ctx);
181     }
182 
183     /**
184      * Sets the values using the informations of the given context.
185      */
186     private void setValues(final Context ctx) throws FileSystemException {
187         // find all setter methods suitable for the given "name"
188         if (!fillConfigSetters(ctx)) {
189             throw new FileSystemException("vfs.provider/config-key-invalid.error", ctx.scheme, ctx.name);
190         }
191 
192         // get the fileSystemConfigBuilder
193         ctx.fileSystemConfigBuilder = getManager().getFileSystemConfigBuilder(ctx.scheme);
194 
195         // try to find a setter which could accept the value
196         for (final Method configSetter : ctx.configSetters) {
197             if (convertValuesAndInvoke(configSetter, ctx)) {
198                 return;
199             }
200         }
201 
202         throw new FileSystemException("vfs.provider/config-value-invalid.error", ctx.scheme, ctx.name, ctx.values);
203     }
204 
205     /**
206      * Tries to convert the value and pass it to the given method
207      */
208     private boolean convertValuesAndInvoke(final Method configSetter, final Context ctx) throws FileSystemException {
209         final Class<?>[] parameters = configSetter.getParameterTypes();
210         if (parameters.length < 2) {
211             return false;
212         }
213         if (!parameters[0].isAssignableFrom(FileSystemOptions.class)) {
214             return false;
215         }
216 
217         final Class<?> valueParameter = parameters[1];
218         Class<?> type;
219         if (valueParameter.isArray()) {
220             type = valueParameter.getComponentType();
221         } else {
222             if (ctx.values.length > 1) {
223                 return false;
224             }
225 
226             type = valueParameter;
227         }
228 
229         if (type.isPrimitive()) {
230             final Class<?> objectType = PRIMATIVE_TO_OBJECT.get(type.getName());
231             if (objectType == null) {
232                 log.warn(Messages.getString("vfs.provider/config-unexpected-primitive.error", type.getName()));
233                 return false;
234             }
235             type = objectType;
236         }
237 
238         final Class<? extends Object> valueClass = ctx.values[0].getClass();
239         if (type.isAssignableFrom(valueClass)) {
240             // can set value directly
241             invokeSetter(valueParameter, ctx, configSetter, ctx.values);
242             return true;
243         }
244         if (valueClass != String.class) {
245             log.warn(Messages.getString("vfs.provider/config-unexpected-value-class.error", valueClass.getName(),
246                     ctx.scheme, ctx.name));
247             return false;
248         }
249 
250         final Object convertedValues = Array.newInstance(type, ctx.values.length);
251 
252         Constructor<?> valueConstructor;
253         try {
254             valueConstructor = type.getConstructor(STRING_PARAM);
255         } catch (final NoSuchMethodException e) {
256             valueConstructor = null;
257         }
258         if (valueConstructor != null) {
259             // can convert using constructor
260             for (int iterValues = 0; iterValues < ctx.values.length; iterValues++) {
261                 try {
262                     Array.set(convertedValues, iterValues, valueConstructor.newInstance(ctx.values[iterValues]));
263                 } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
264                     throw new FileSystemException(e);
265                 }
266             }
267 
268             invokeSetter(valueParameter, ctx, configSetter, convertedValues);
269             return true;
270         }
271 
272         Method valueFactory;
273         try {
274             valueFactory = type.getMethod("valueOf", STRING_PARAM);
275             if (!Modifier.isStatic(valueFactory.getModifiers())) {
276                 valueFactory = null;
277             }
278         } catch (final NoSuchMethodException e) {
279             valueFactory = null;
280         }
281 
282         if (valueFactory != null) {
283             // can convert using factory method (valueOf)
284             for (int iterValues = 0; iterValues < ctx.values.length; iterValues++) {
285                 try {
286                     Array.set(convertedValues, iterValues,
287                             valueFactory.invoke(null, ctx.values[iterValues]));
288                 } catch (final IllegalAccessException | InvocationTargetException e) {
289                     throw new FileSystemException(e);
290                 }
291             }
292 
293             invokeSetter(valueParameter, ctx, configSetter, convertedValues);
294             return true;
295         }
296 
297         return false;
298     }
299 
300     /**
301      * Invokes the method with the converted values
302      */
303     private void invokeSetter(final Class<?> valueParameter, final Context ctx, final Method configSetter,
304             final Object values) throws FileSystemException {
305         final Object[] args;
306         if (valueParameter.isArray()) {
307             args = new Object[] { ctx.fso, values };
308         } else {
309             args = new Object[] { ctx.fso, Array.get(values, 0) };
310         }
311         try {
312             configSetter.invoke(ctx.fileSystemConfigBuilder, args);
313         } catch (final IllegalAccessException | InvocationTargetException e) {
314             throw new FileSystemException(e);
315         }
316     }
317 
318     /**
319      * Fills all available set*() methods for the context-scheme into the context.
320      */
321     private boolean fillConfigSetters(final Context ctx) throws FileSystemException {
322         final Map<String, List<Method>> schemeMethods = getSchemeMethods(ctx.scheme);
323         final List<Method> configSetters = schemeMethods.get(ctx.name.toLowerCase());
324         if (configSetters == null) {
325             return false;
326         }
327 
328         ctx.configSetters = configSetters;
329         return true;
330     }
331 
332     /**
333      * Gets (cached) list of set*() methods for the given scheme
334      */
335     private Map<String, List<Method>> getSchemeMethods(final String scheme) throws FileSystemException {
336         Map<String, List<Method>> schemeMethods = beanMethods.get(scheme);
337         if (schemeMethods == null) {
338             schemeMethods = createSchemeMethods(scheme);
339             beanMethods.put(scheme, schemeMethods);
340         }
341 
342         return schemeMethods;
343     }
344 
345     /**
346      * Creates the list of all set*() methods for the given scheme
347      */
348     private Map<String, List<Method>> createSchemeMethods(final String scheme) throws FileSystemException {
349         final FileSystemConfigBuilder fscb = getManager().getFileSystemConfigBuilder(scheme);
350         FileSystemException.requireNonNull(fscb, "vfs.provider/no-config-builder.error", scheme);
351 
352         final Map<String, List<Method>> schemeMethods = new TreeMap<>();
353 
354         final Method[] methods = fscb.getClass().getMethods();
355         for (final Method method : methods) {
356             if (!Modifier.isPublic(method.getModifiers())) {
357                 continue;
358             }
359 
360             final String methodName = method.getName();
361             if (!methodName.startsWith("set")) {
362                 // not a setter
363                 continue;
364             }
365 
366             final String key = methodName.substring(3).toLowerCase();
367 
368             final List<Method> configSetter = schemeMethods.computeIfAbsent(key, k -> new ArrayList<>(2));
369             configSetter.add(method);
370         }
371 
372         return schemeMethods;
373     }
374 }