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}