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}