/* * Syncany, www.syncany.org * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.syncany.plugins.transfer; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import org.simpleframework.xml.Attribute; import org.simpleframework.xml.Element; import org.simpleframework.xml.core.Validate; import org.syncany.config.UserConfig; import org.syncany.crypto.CipherException; import org.syncany.crypto.CipherSpecs; import org.syncany.crypto.CipherUtil; import org.syncany.plugins.Plugin; import org.syncany.plugins.UserInteractionListener; import org.syncany.util.ReflectionUtil; import org.syncany.util.StringUtil; import com.google.common.base.Objects; /** * A connection represents the configuration settings of a storage/connection * plugin. It is created through the concrete implementation of a {@link Plugin}. * * <p>Options for a plugin specific {@link TransferSettings} can be defined using the * {@link Element} annotation. Furthermore some Syncany-specific annotations are available. * * @author Philipp C. Heckel <philipp.heckel@gmail.com> * @author Christian Roth <christian.roth@port17.de> */ public abstract class TransferSettings { private static final Logger logger = Logger.getLogger(TransferSettings.class.getName()); @Attribute private String type = findPluginId(); private String lastValidationFailReason; private UserInteractionListener userInteractionListener; public UserInteractionListener getUserInteractionListener() { return userInteractionListener; } public void setUserInteractionListener(UserInteractionListener userInteractionListener) { this.userInteractionListener = userInteractionListener; } public final String getType() { return type; } /** * Get a setting's value. * * @param key The field name as it is used in the {@link TransferSettings} * @return The value converted to a string using {@link Class#toString()} * @throws StorageException Thrown if the field either does not exist or isn't accessible */ public final String getField(String key) throws StorageException { try { Field field = this.getClass().getDeclaredField(key); field.setAccessible(true); Object fieldValueAsObject = field.get(this); if (fieldValueAsObject == null) { return null; } return fieldValueAsObject.toString(); } catch (NoSuchFieldException | IllegalAccessException e) { throw new StorageException("Unable to getField named " + key + ": " + e.getMessage()); } } /** * Set the value of a field in the settings class. * * @param key The field name as it is used in the {@link TransferSettings} * @param value The object which should be the setting's value. The object's type must match the field type. * {@link Integer}, {@link String}, {@link Boolean}, {@link File} and implementation of * {@link TransferSettings} are converted. * @throws StorageException Thrown if the field either does not exist or isn't accessible or * conversion failed due to invalid field types. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public final void setField(String key, Object value) throws StorageException { try { Field[] elementFields = ReflectionUtil.getAllFieldsWithAnnotation(this.getClass(), Element.class); for (Field field : elementFields) { field.setAccessible(true); String fieldName = field.getName(); Type fieldType = field.getType(); if (key.equalsIgnoreCase(fieldName)) { if (value == null) { field.set(this, null); } else if (fieldType == Integer.TYPE && (value instanceof Integer || value instanceof String)) { field.setInt(this, Integer.parseInt(String.valueOf(value))); } else if (fieldType == Boolean.TYPE && (value instanceof Boolean || value instanceof String)) { field.setBoolean(this, Boolean.parseBoolean(String.valueOf(value))); } else if (fieldType == String.class && value instanceof String) { field.set(this, value); } else if (fieldType == File.class && value instanceof String) { field.set(this, new File(String.valueOf(value))); } else if (ReflectionUtil.getClassFromType(fieldType).isEnum() && value instanceof String) { Class<? extends Enum> enumClass = (Class<? extends Enum>) ReflectionUtil.getClassFromType(fieldType); String enumValue = String.valueOf(value).toUpperCase(); Enum translatedEnum = Enum.valueOf(enumClass, enumValue); field.set(this, translatedEnum); } else if (TransferSettings.class.isAssignableFrom(value.getClass())) { field.set(this, ReflectionUtil.getClassFromType(fieldType).cast(value)); } else { throw new RuntimeException("Invalid value type: " + value.getClass()); } } } } catch (Exception e) { throw new StorageException("Unable to parse value because its format is invalid: " + e.getMessage(), e); } } /** * Check if a {@link TransferSettings} instance is valid i.e. all required fields are present. * {@link TransferSettings} specific validators can be deposited by annotating a method with {@link Validate}. * * @return True if the {@link TransferSettings} instance is valid. */ public final boolean isValid() { Method[] validationMethods = ReflectionUtil.getAllMethodsWithAnnotation(this.getClass(), Validate.class); try { for (Method method : validationMethods) { method.setAccessible(true); method.invoke(this); } } catch (InvocationTargetException | IllegalAccessException e) { logger.log(Level.SEVERE, "Unable to check if option(s) are valid.", e); if (e.getCause() instanceof StorageException) { // Dirty hack lastValidationFailReason = e.getCause().getMessage(); return false; } throw new RuntimeException("Unable to call plugin validator: ", e); } return true; } /** * Get the reason why the validation with {@link TransferSettings#isValid()} failed. * * @return The first reason why the validation process failed */ public final String getReasonForLastValidationFail() { return lastValidationFailReason; } /** * Validate if all required fields are present. * * @throws StorageException Thrown if the validation failed due to missing field values. */ @Validate public final void validateRequiredFields() throws StorageException { logger.log(Level.FINE, "Validating required fields"); try { Field[] elementFields = ReflectionUtil.getAllFieldsWithAnnotation(this.getClass(), Element.class); for (Field field : elementFields) { field.setAccessible(true); if (field.getAnnotation(Element.class).required() && field.get(this) == null) { logger.log(Level.WARNING, "Missing mandatory field {0}#{1}", new Object[] { this.getClass().getSimpleName(), field.getName() }); throw new StorageException("Missing mandatory field " + this.getClass().getSimpleName() + "#" + field.getName()); } } } catch (IllegalAccessException e) { throw new RuntimeException("IllegalAccessException when validating required fields: ", e); } } private String findPluginId() { Class<? extends TransferPlugin> transferPluginClass = TransferPluginUtil.getTransferPluginClass(this.getClass()); try { if (transferPluginClass != null) { return transferPluginClass.newInstance().getId(); } throw new RuntimeException("Unable to read type: No TransferPlugin is defined for these settings"); } catch (Exception e) { logger.log(Level.SEVERE, "Unable to read type: No TransferPlugin is defined for these settings", e); throw new RuntimeException("Unable to read type: No TransferPlugin is defined for these settings", e); } } @Override public String toString() { Objects.ToStringHelper toStringHelper = Objects.toStringHelper(this); for (Field field : ReflectionUtil.getAllFieldsWithAnnotation(this.getClass(), Element.class)) { field.setAccessible(true); try { toStringHelper.add(field.getName(), field.get(this)); } catch (IllegalAccessException e) { logger.log(Level.FINE, "Field is unaccessable", e); toStringHelper.add(field.getName(), "**IllegalAccessException**"); } } return toStringHelper.toString(); } public static String decrypt(String encryptedHexString) throws CipherException { byte[] encryptedBytes = StringUtil.fromHex(encryptedHexString); byte[] decryptedBytes = CipherUtil.decrypt(new ByteArrayInputStream(encryptedBytes), UserConfig.getConfigEncryptionKey()); return new String(decryptedBytes); } public static String encrypt(String decryptedPlainString) throws CipherException { InputStream plaintextInputStream = IOUtils.toInputStream(decryptedPlainString); byte[] encryptedBytes = CipherUtil.encrypt(plaintextInputStream, CipherSpecs.getDefaultCipherSpecs(), UserConfig.getConfigEncryptionKey()); return StringUtil.toHex(encryptedBytes); } }