001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.plugins.transfer; 019 020import java.io.ByteArrayInputStream; 021import java.io.File; 022import java.io.InputStream; 023import java.lang.reflect.Field; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.lang.reflect.Type; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import com.google.common.base.MoreObjects; 031import org.apache.commons.io.IOUtils; 032import org.simpleframework.xml.Attribute; 033import org.simpleframework.xml.Element; 034import org.simpleframework.xml.core.Validate; 035import org.syncany.config.UserConfig; 036import org.syncany.crypto.CipherException; 037import org.syncany.crypto.CipherSpecs; 038import org.syncany.crypto.CipherUtil; 039import org.syncany.plugins.Plugin; 040import org.syncany.plugins.UserInteractionListener; 041import org.syncany.util.ReflectionUtil; 042import org.syncany.util.StringUtil; 043 044/** 045 * A connection represents the configuration settings of a storage/connection 046 * plugin. It is created through the concrete implementation of a {@link Plugin}. 047 * 048 * <p>Options for a plugin specific {@link TransferSettings} can be defined using the 049 * {@link Element} annotation. Furthermore some Syncany-specific annotations are available. 050 * 051 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 052 * @author Christian Roth (christian.roth@port17.de) 053 */ 054public abstract class TransferSettings { 055 private static final Logger logger = Logger.getLogger(TransferSettings.class.getName()); 056 057 @Attribute 058 private String type = findPluginId(); 059 060 private String lastValidationFailReason; 061 private UserInteractionListener userInteractionListener; 062 063 public UserInteractionListener getUserInteractionListener() { 064 return userInteractionListener; 065 } 066 067 public void setUserInteractionListener(UserInteractionListener userInteractionListener) { 068 this.userInteractionListener = userInteractionListener; 069 } 070 071 public final String getType() { 072 return type; 073 } 074 075 /** 076 * Get a setting's value. 077 * 078 * @param key The field name as it is used in the {@link TransferSettings} 079 * @return The value converted to a string using {@link Class#toString()} 080 * @throws StorageException Thrown if the field either does not exist or isn't accessible 081 */ 082 public final String getField(String key) throws StorageException { 083 try { 084 Field field = this.getClass().getDeclaredField(key); 085 field.setAccessible(true); 086 087 Object fieldValueAsObject = field.get(this); 088 089 if (fieldValueAsObject == null) { 090 return null; 091 } 092 093 return fieldValueAsObject.toString(); 094 } 095 catch (NoSuchFieldException | IllegalAccessException e) { 096 throw new StorageException("Unable to getField named " + key + ": " + e.getMessage()); 097 } 098 } 099 100 /** 101 * Set the value of a field in the settings class. 102 * 103 * @param key The field name as it is used in the {@link TransferSettings} 104 * @param value The object which should be the setting's value. The object's type must match the field type. 105 * {@link Integer}, {@link String}, {@link Boolean}, {@link File} and implementation of 106 * {@link TransferSettings} are converted. 107 * @throws StorageException Thrown if the field either does not exist or isn't accessible or 108 * conversion failed due to invalid field types. 109 */ 110 @SuppressWarnings({ "rawtypes", "unchecked" }) 111 public final void setField(String key, Object value) throws StorageException { 112 try { 113 Field[] elementFields = ReflectionUtil.getAllFieldsWithAnnotation(this.getClass(), Element.class); 114 115 for (Field field : elementFields) { 116 field.setAccessible(true); 117 118 String fieldName = field.getName(); 119 Type fieldType = field.getType(); 120 121 if (key.equalsIgnoreCase(fieldName)) { 122 if (value == null) { 123 field.set(this, null); 124 } 125 else if (fieldType == Integer.TYPE && (value instanceof Integer || value instanceof String)) { 126 field.setInt(this, Integer.parseInt(String.valueOf(value))); 127 } 128 else if (fieldType == Boolean.TYPE && (value instanceof Boolean || value instanceof String)) { 129 field.setBoolean(this, Boolean.parseBoolean(String.valueOf(value))); 130 } 131 else if (fieldType == String.class && value instanceof String) { 132 field.set(this, value); 133 } 134 else if (fieldType == File.class && value instanceof String) { 135 field.set(this, new File(String.valueOf(value))); 136 } 137 else if (ReflectionUtil.getClassFromType(fieldType).isEnum() && value instanceof String) { 138 Class<? extends Enum> enumClass = (Class<? extends Enum>) ReflectionUtil.getClassFromType(fieldType); 139 String enumValue = String.valueOf(value).toUpperCase(); 140 141 Enum translatedEnum = Enum.valueOf(enumClass, enumValue); 142 field.set(this, translatedEnum); 143 } 144 else if (TransferSettings.class.isAssignableFrom(value.getClass())) { 145 field.set(this, ReflectionUtil.getClassFromType(fieldType).cast(value)); 146 } 147 else { 148 throw new RuntimeException("Invalid value type: " + value.getClass()); 149 } 150 } 151 } 152 } 153 catch (Exception e) { 154 throw new StorageException("Unable to parse value because its format is invalid: " + e.getMessage(), e); 155 } 156 } 157 158 /** 159 * Check if a {@link TransferSettings} instance is valid i.e. all required fields are present. 160 * {@link TransferSettings} specific validators can be deposited by annotating a method with {@link Validate}. 161 * 162 * @return True if the {@link TransferSettings} instance is valid. 163 */ 164 public final boolean isValid() { 165 Method[] validationMethods = ReflectionUtil.getAllMethodsWithAnnotation(this.getClass(), Validate.class); 166 167 try { 168 for (Method method : validationMethods) { 169 method.setAccessible(true); 170 method.invoke(this); 171 } 172 } 173 catch (InvocationTargetException | IllegalAccessException e) { 174 logger.log(Level.SEVERE, "Unable to check if option(s) are valid.", e); 175 176 if (e.getCause() instanceof StorageException) { // Dirty hack 177 lastValidationFailReason = e.getCause().getMessage(); 178 return false; 179 } 180 181 throw new RuntimeException("Unable to call plugin validator: ", e); 182 } 183 184 return true; 185 } 186 187 /** 188 * Get the reason why the validation with {@link TransferSettings#isValid()} failed. 189 * 190 * @return The first reason why the validation process failed 191 */ 192 public final String getReasonForLastValidationFail() { 193 return lastValidationFailReason; 194 } 195 196 /** 197 * Validate if all required fields are present. 198 * 199 * @throws StorageException Thrown if the validation failed due to missing field values. 200 */ 201 @Validate 202 public final void validateRequiredFields() throws StorageException { 203 logger.log(Level.FINE, "Validating required fields"); 204 205 try { 206 Field[] elementFields = ReflectionUtil.getAllFieldsWithAnnotation(this.getClass(), Element.class); 207 208 for (Field field : elementFields) { 209 field.setAccessible(true); 210 211 if (field.getAnnotation(Element.class).required() && field.get(this) == null) { 212 logger.log(Level.WARNING, "Missing mandatory field {0}#{1}", new Object[] { this.getClass().getSimpleName(), field.getName() }); 213 throw new StorageException("Missing mandatory field " + this.getClass().getSimpleName() + "#" + field.getName()); 214 } 215 } 216 } 217 catch (IllegalAccessException e) { 218 throw new RuntimeException("IllegalAccessException when validating required fields: ", e); 219 } 220 } 221 222 private String findPluginId() { 223 Class<? extends TransferPlugin> transferPluginClass = TransferPluginUtil.getTransferPluginClass(this.getClass()); 224 225 try { 226 if (transferPluginClass != null) { 227 return transferPluginClass.newInstance().getId(); 228 } 229 230 throw new RuntimeException("Unable to read type: No TransferPlugin is defined for these settings"); 231 } 232 catch (Exception e) { 233 logger.log(Level.SEVERE, "Unable to read type: No TransferPlugin is defined for these settings", e); 234 throw new RuntimeException("Unable to read type: No TransferPlugin is defined for these settings", e); 235 } 236 } 237 238 @Override 239 public String toString() { 240 MoreObjects.ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); 241 242 for (Field field : ReflectionUtil.getAllFieldsWithAnnotation(this.getClass(), Element.class)) { 243 field.setAccessible(true); 244 245 try { 246 toStringHelper.add(field.getName(), field.get(this)); 247 } 248 catch (IllegalAccessException e) { 249 logger.log(Level.FINE, "Field is unaccessable", e); 250 toStringHelper.add(field.getName(), "**IllegalAccessException**"); 251 } 252 } 253 254 return toStringHelper.toString(); 255 } 256 257 public static String decrypt(String encryptedHexString) throws CipherException { 258 byte[] encryptedBytes = StringUtil.fromHex(encryptedHexString); 259 byte[] decryptedBytes = CipherUtil.decrypt(new ByteArrayInputStream(encryptedBytes), UserConfig.getConfigEncryptionKey()); 260 261 return new String(decryptedBytes); 262 } 263 264 public static String encrypt(String decryptedPlainString) throws CipherException { 265 InputStream plaintextInputStream = IOUtils.toInputStream(decryptedPlainString); 266 byte[] encryptedBytes = CipherUtil.encrypt(plaintextInputStream, CipherSpecs.getDefaultCipherSpecs(), UserConfig.getConfigEncryptionKey()); 267 268 return StringUtil.toHex(encryptedBytes); 269 } 270}