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.List;
026import java.util.Map;
027import java.util.TreeMap;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.apache.commons.vfs2.FileSystemConfigBuilder;
032import org.apache.commons.vfs2.FileSystemException;
033import org.apache.commons.vfs2.FileSystemManager;
034import org.apache.commons.vfs2.FileSystemOptions;
035
036/**
037 * This class use reflection to set a configuration value using the fileSystemConfigBuilder associated the a scheme.
038 * <p>
039 * Example:
040 * </p>
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
052    /**
053     * Context.
054     */
055    private static final class Context {
056        private final FileSystemOptions fso;
057        private final String scheme;
058        private final String name;
059        private final Object[] values;
060
061        private List<Method> configSetters;
062        private FileSystemConfigBuilder fileSystemConfigBuilder;
063
064        private Context(final FileSystemOptions fso, final String scheme, final String name, final Object[] values) {
065            this.fso = fso;
066            this.scheme = scheme;
067            this.name = name;
068            this.values = values;
069        }
070    }
071    @SuppressWarnings("unchecked") // OK, it is a String
072    private static final Class<String>[] STRING_PARAM = new Class[] {String.class};
073    private static final Map<String, Class<?>> PRIMITIVE_TO_OBJECT = new TreeMap<>();
074
075    private static final Log log = LogFactory.getLog(DelegatingFileSystemOptionsBuilder.class);
076    static {
077        PRIMITIVE_TO_OBJECT.put(Void.TYPE.getName(), Void.class);
078        PRIMITIVE_TO_OBJECT.put(Boolean.TYPE.getName(), Boolean.class);
079        PRIMITIVE_TO_OBJECT.put(Byte.TYPE.getName(), Byte.class);
080        PRIMITIVE_TO_OBJECT.put(Character.TYPE.getName(), Character.class);
081        PRIMITIVE_TO_OBJECT.put(Short.TYPE.getName(), Short.class);
082        PRIMITIVE_TO_OBJECT.put(Integer.TYPE.getName(), Integer.class);
083        PRIMITIVE_TO_OBJECT.put(Long.TYPE.getName(), Long.class);
084        PRIMITIVE_TO_OBJECT.put(Double.TYPE.getName(), Double.class);
085        PRIMITIVE_TO_OBJECT.put(Float.TYPE.getName(), Float.class);
086    }
087
088    private final FileSystemManager manager;
089
090    private final Map<String, Map<String, List<Method>>> beanMethods = new TreeMap<>();
091
092    /**
093     * Constructs a new instance.
094     * <p>
095     * Pass in your fileSystemManager instance.
096     * </p>
097     *
098     * @param manager the manager to use to get the fileSystemConfigBuilder associated to a scheme
099     */
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}