/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 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);
}
}