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