001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.util;
018
019import java.lang.reflect.Array;
020import java.lang.reflect.Constructor;
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.Method;
023import java.lang.reflect.Modifier;
024import java.util.ArrayList;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.TreeMap;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.apache.commons.vfs2.FileSystemConfigBuilder;
033import org.apache.commons.vfs2.FileSystemException;
034import org.apache.commons.vfs2.FileSystemManager;
035import org.apache.commons.vfs2.FileSystemOptions;
036
037/**
038 * This class use reflection to set a configuration value using the fileSystemConfigBuilder associated the a scheme.<br>
039 * <br>
040 * Example:<br>
041 *
042 * <pre>
043 * FileSystemOptions fso = new FileSystemOptions();
044 * DelegatingFileSystemOptionsBuilder delegate = new DelegatingFileSystemOptionsBuilder(VFS.getManager());
045 * delegate.setConfigString(fso, "sftp", "identities", "c:/tmp/test.ident");
046 * delegate.setConfigString(fso, "http", "proxyPort", "8080");
047 * delegate.setConfigClass(fso, "sftp", "userinfo", TrustEveryoneUserInfo.class);
048 * </pre>
049 */
050public class DelegatingFileSystemOptionsBuilder {
051    @SuppressWarnings("unchecked") // OK, it is a String
052    private static final Class<String>[] STRING_PARAM = new Class[] { String.class };
053    private static final Map<String, Class<?>> PRIMATIVE_TO_OBJECT = new TreeMap<>();
054    private static final Log log = LogFactory.getLog(DelegatingFileSystemOptionsBuilder.class);
055
056    private final FileSystemManager manager;
057    private final Map<String, Map<String, List<Method>>> beanMethods = new TreeMap<>();
058
059    static {
060        PRIMATIVE_TO_OBJECT.put(Void.TYPE.getName(), Void.class);
061        PRIMATIVE_TO_OBJECT.put(Boolean.TYPE.getName(), Boolean.class);
062        PRIMATIVE_TO_OBJECT.put(Byte.TYPE.getName(), Byte.class);
063        PRIMATIVE_TO_OBJECT.put(Character.TYPE.getName(), Character.class);
064        PRIMATIVE_TO_OBJECT.put(Short.TYPE.getName(), Short.class);
065        PRIMATIVE_TO_OBJECT.put(Integer.TYPE.getName(), Integer.class);
066        PRIMATIVE_TO_OBJECT.put(Long.TYPE.getName(), Long.class);
067        PRIMATIVE_TO_OBJECT.put(Double.TYPE.getName(), Double.class);
068        PRIMATIVE_TO_OBJECT.put(Float.TYPE.getName(), Float.class);
069    }
070
071    /**
072     * Context.
073     */
074    private static final class Context {
075        private final FileSystemOptions fso;
076        private final String scheme;
077        private final String name;
078        private final Object[] values;
079
080        private List<Method> configSetters;
081        private FileSystemConfigBuilder fileSystemConfigBuilder;
082
083        private Context(final FileSystemOptions fso, final String scheme, final String name, final Object[] values) {
084            this.fso = fso;
085            this.scheme = scheme;
086            this.name = name;
087            this.values = values;
088        }
089    }
090
091    /**
092     * Constructor.<br>
093     * Pass in your fileSystemManager instance.
094     *
095     * @param manager the manager to use to get the fileSystemConfigBuilder assocated to a scheme
096     */
097    public DelegatingFileSystemOptionsBuilder(final FileSystemManager manager) {
098        this.manager = manager;
099    }
100
101    protected FileSystemManager getManager() {
102        return manager;
103    }
104
105    /**
106     * Set a single string value.
107     *
108     * @param fso FileSystemOptions
109     * @param scheme scheme
110     * @param name name
111     * @param value value
112     * @throws FileSystemException if an error occurs.
113     */
114    public void setConfigString(final FileSystemOptions fso, final String scheme, final String name, final String value)
115            throws FileSystemException {
116        setConfigStrings(fso, scheme, name, new String[] { value });
117    }
118
119    /**
120     * Set an array of string value.
121     *
122     * @param fso FileSystemOptions
123     * @param scheme scheme
124     * @param name name
125     * @param values values
126     * @throws FileSystemException if an error occurs.
127     */
128    public void setConfigStrings(final FileSystemOptions fso, final String scheme, final String name,
129            final String[] values) throws FileSystemException {
130        final Context ctx = new Context(fso, scheme, name, values);
131
132        setValues(ctx);
133    }
134
135    /**
136     * Set a single class value.<br>
137     * The class has to implement a no-args constructor, else the instantiation might fail.
138     *
139     * @param fso FileSystemOptions
140     * @param scheme scheme
141     * @param name name
142     * @param className className
143     * @throws FileSystemException if an error occurs.
144     * @throws IllegalAccessException if a class canoot be accessed.
145     * @throws InstantiationException if a class cannot be instantiated.
146     */
147    public void setConfigClass(final FileSystemOptions fso, final String scheme, final String name,
148            final Class<?> className) throws FileSystemException, IllegalAccessException, InstantiationException {
149        setConfigClasses(fso, scheme, name, new Class[] { className });
150    }
151
152    /**
153     * Set an array of class values.<br>
154     * The class has to implement a no-args constructor, else the instantiation might fail.
155     *
156     * @param fso FileSystemOptions
157     * @param scheme scheme
158     * @param name name
159     * @param classNames classNames
160     * @throws FileSystemException if an error occurs.
161     * @throws IllegalAccessException if a class canoot be accessed.
162     * @throws InstantiationException if a class cannot be instantiated.
163     */
164    public void setConfigClasses(final FileSystemOptions fso, final String scheme, final String name,
165            final Class<?>[] classNames) throws FileSystemException, IllegalAccessException, InstantiationException {
166        final Object[] values = new Object[classNames.length];
167        for (int iterClassNames = 0; iterClassNames < values.length; iterClassNames++) {
168            values[iterClassNames] = classNames[iterClassNames].newInstance();
169        }
170
171        final Context ctx = new Context(fso, scheme, name, values);
172
173        setValues(ctx);
174    }
175
176    /**
177     * sets the values using the informations of the given context.<br>
178     */
179    private void setValues(final Context ctx) throws FileSystemException {
180        // find all setter methods suitable for the given "name"
181        if (!fillConfigSetters(ctx)) {
182            throw new FileSystemException("vfs.provider/config-key-invalid.error", ctx.scheme, ctx.name);
183        }
184
185        // get the fileSystemConfigBuilder
186        ctx.fileSystemConfigBuilder = getManager().getFileSystemConfigBuilder(ctx.scheme);
187
188        // try to find a setter which could accept the value
189        final Iterator<Method> iterConfigSetters = ctx.configSetters.iterator();
190        while (iterConfigSetters.hasNext()) {
191            final Method configSetter = iterConfigSetters.next();
192            if (convertValuesAndInvoke(configSetter, ctx)) {
193                return;
194            }
195        }
196
197        throw new FileSystemException("vfs.provider/config-value-invalid.error", ctx.scheme, ctx.name, ctx.values);
198    }
199
200    /**
201     * tries to convert the value and pass it to the given method
202     */
203    private boolean convertValuesAndInvoke(final Method configSetter, final Context ctx) throws FileSystemException {
204        final Class<?>[] parameters = configSetter.getParameterTypes();
205        if (parameters.length < 2) {
206            return false;
207        }
208        if (!parameters[0].isAssignableFrom(FileSystemOptions.class)) {
209            return false;
210        }
211
212        final Class<?> valueParameter = parameters[1];
213        Class<?> type;
214        if (valueParameter.isArray()) {
215            type = valueParameter.getComponentType();
216        } else {
217            if (ctx.values.length > 1) {
218                return false;
219            }
220
221            type = valueParameter;
222        }
223
224        if (type.isPrimitive()) {
225            final Class<?> objectType = PRIMATIVE_TO_OBJECT.get(type.getName());
226            if (objectType == null) {
227                log.warn(Messages.getString("vfs.provider/config-unexpected-primitive.error", type.getName()));
228                return false;
229            }
230            type = objectType;
231        }
232
233        final Class<? extends Object> valueClass = ctx.values[0].getClass();
234        if (type.isAssignableFrom(valueClass)) {
235            // can set value directly
236            invokeSetter(valueParameter, ctx, configSetter, ctx.values);
237            return true;
238        }
239        if (valueClass != String.class) {
240            log.warn(Messages.getString("vfs.provider/config-unexpected-value-class.error", valueClass.getName(),
241                    ctx.scheme, ctx.name));
242            return false;
243        }
244
245        final Object convertedValues = Array.newInstance(type, ctx.values.length);
246
247        Constructor<?> valueConstructor;
248        try {
249            valueConstructor = type.getConstructor(STRING_PARAM);
250        } catch (final NoSuchMethodException e) {
251            valueConstructor = null;
252        }
253        if (valueConstructor != null) {
254            // can convert using constructor
255            for (int iterValues = 0; iterValues < ctx.values.length; iterValues++) {
256                try {
257                    Array.set(convertedValues, iterValues,
258                            valueConstructor.newInstance(new Object[] { ctx.values[iterValues] }));
259                } catch (final InstantiationException e) {
260                    throw new FileSystemException(e);
261                } catch (final IllegalAccessException e) {
262                    throw new FileSystemException(e);
263                } catch (final 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, new Object[] { ctx.values[iterValues] }));
288                } catch (final IllegalAccessException e) {
289                    throw new FileSystemException(e);
290                } catch (final InvocationTargetException e) {
291                    throw new FileSystemException(e);
292                }
293            }
294
295            invokeSetter(valueParameter, ctx, configSetter, convertedValues);
296            return true;
297        }
298
299        return false;
300    }
301
302    /**
303     * invokes the method with the converted values
304     */
305    private void invokeSetter(final Class<?> valueParameter, final Context ctx, final Method configSetter,
306            final Object values) throws FileSystemException {
307        Object[] args;
308        if (valueParameter.isArray()) {
309            args = new Object[] { ctx.fso, values };
310        } else {
311            args = new Object[] { ctx.fso, Array.get(values, 0) };
312        }
313        try {
314            configSetter.invoke(ctx.fileSystemConfigBuilder, args);
315        } catch (final IllegalAccessException e) {
316            throw new FileSystemException(e);
317        } catch (final InvocationTargetException e) {
318            throw new FileSystemException(e);
319        }
320    }
321
322    /**
323     * fills all available set*() methods for the context-scheme into the context.
324     */
325    private boolean fillConfigSetters(final Context ctx) throws FileSystemException {
326        final Map<String, List<Method>> schemeMethods = getSchemeMethods(ctx.scheme);
327        final List<Method> configSetters = schemeMethods.get(ctx.name.toLowerCase());
328        if (configSetters == null) {
329            return false;
330        }
331
332        ctx.configSetters = configSetters;
333        return true;
334    }
335
336    /**
337     * get (cached) list of set*() methods for the given scheme
338     */
339    private Map<String, List<Method>> getSchemeMethods(final String scheme) throws FileSystemException {
340        Map<String, List<Method>> schemeMethods = beanMethods.get(scheme);
341        if (schemeMethods == null) {
342            schemeMethods = createSchemeMethods(scheme);
343            beanMethods.put(scheme, schemeMethods);
344        }
345
346        return schemeMethods;
347    }
348
349    /**
350     * create the list of all set*() methods for the given scheme
351     */
352    private Map<String, List<Method>> createSchemeMethods(final String scheme) throws FileSystemException {
353        final FileSystemConfigBuilder fscb = getManager().getFileSystemConfigBuilder(scheme);
354        if (fscb == null) {
355            throw new FileSystemException("vfs.provider/no-config-builder.error", scheme);
356        }
357
358        final Map<String, List<Method>> schemeMethods = new TreeMap<>();
359
360        final Method[] methods = fscb.getClass().getMethods();
361        for (final Method method : methods) {
362            if (!Modifier.isPublic(method.getModifiers())) {
363                continue;
364            }
365
366            final String methodName = method.getName();
367            if (!methodName.startsWith("set")) {
368                // not a setter
369                continue;
370            }
371
372            final String key = methodName.substring(3).toLowerCase();
373
374            List<Method> configSetter = schemeMethods.get(key);
375            if (configSetter == null) {
376                configSetter = new ArrayList<>(2);
377                schemeMethods.put(key, configSetter);
378            }
379            configSetter.add(method);
380        }
381
382        return schemeMethods;
383    }
384}