org.syncany.plugins.transfer.TransferSettings.java Source code

Java tutorial

Introduction

Here is the source code for org.syncany.plugins.transfer.TransferSettings.java

Source

/*
 * 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);
    }
}