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.lang.annotation.Annotation;
021import java.lang.reflect.Constructor;
022import java.lang.reflect.InvocationTargetException;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.syncany.config.Config;
029import org.syncany.plugins.transfer.features.ReadAfterWriteConsistent;
030import org.syncany.plugins.transfer.features.Feature;
031import org.syncany.plugins.transfer.features.FeatureTransferManager;
032import org.syncany.plugins.transfer.features.PathAware;
033import org.syncany.plugins.transfer.features.Retriable;
034import org.syncany.plugins.transfer.features.TransactionAware;
035import org.syncany.util.ReflectionUtil;
036import com.google.common.collect.ImmutableList;
037import com.google.common.collect.Sets;
038
039/**
040 * This factory class creates a {@link TransferManager} from a
041 * {@link Config} object, and wraps it into the requested {@link Feature}(s).
042 *
043 * <p>Depending on the {@link Feature}s that the original transfer manager is
044 * annotated with, the factory will wrap it into the corresponding feature
045 * specific transfer managers.
046 *
047 * <p>The class uses the builder pattern. It can be used like this:
048 *
049 * <pre>
050 *   TransactionAwareFeatureTransferManager txAwareTM = TransferManagerFactory
051 *     .build(config)
052 *     .withFeature(Retriable.class)
053 *     .withFeature(PathAware.class)
054 *     .withFeature(TransactionAware.class)
055 *     .as(TransactionAware.class);
056 * </pre>
057 *
058 * @see Feature
059 * @see FeatureTransferManager
060 * @see TransferManager
061 * @author Christian Roth (christian.roth@port17.de)
062 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
063 */
064public class TransferManagerFactory {
065        private static final Logger logger = Logger.getLogger(TransferManagerFactory.class.getSimpleName());
066
067        private static final String FEATURE_TRANSFER_MANAGER_FORMAT = Feature.class.getPackage().getName() + ".%s" + FeatureTransferManager.class.getSimpleName();
068        private static final List<Class<? extends Annotation>> FEATURE_LIST = ImmutableList.<Class<? extends Annotation>> builder()
069                        .add(TransactionAware.class)
070                        .add(Retriable.class)
071                        .add(PathAware.class)
072                        .add(ReadAfterWriteConsistent.class)
073                        .build();
074
075        /**
076         * Creates the transfer manager factory builder from the {@link Config}
077         * using the configured {@link TransferPlugin}. Using this builder, the
078         * feature-wrapped transfer manager can be built.
079         *
080         * @see TransferManagerBuilder
081         * @param config Local folder configuration with transfer plugin settings
082         * @return Transfer manager builder
083         */
084        public static TransferManagerBuilder build(Config config) throws StorageException {
085                TransferManager transferManager = config.getTransferPlugin().createTransferManager(config.getConnection(), config);
086                logger.log(Level.INFO, "Building " + transferManager.getClass().getSimpleName() + " from config '" + config.getLocalDir().getName() + "' ...");
087
088                return new TransferManagerBuilder(config, transferManager);
089        }
090
091        /**
092         * The transfer manager builder takes an original {@link TransferManager}, and
093         * wraps it with feature-specific transfer managers, if the original transfer
094         * manager is annotated with a {@link Feature} annotation.
095         *
096         * <p>The class uses the builder pattern. Its usage is described in the
097         * {@link TransferManagerFactory}. The two main methods of this class are
098         * {@link #withFeature(Class)} and {@link #as(Class)}.
099         *
100         * @see Feature
101         * @see TransferManagerFactory
102         */
103        public static class TransferManagerBuilder {
104                private List<Class<? extends Annotation>> features;
105                private Config config;
106                private TransferManager originalTransferManager;
107                private TransferManager wrappedTransferManager;
108
109                private TransferManagerBuilder(Config config, TransferManager transferManager) {
110                        this.config = config;
111                        this.originalTransferManager = transferManager;
112                        this.wrappedTransferManager = transferManager;
113                        this.features = new ArrayList<>();
114                }
115
116                /**
117                 * This method requests the original transfer manager to be wrapped in the corresponding
118                 * feature transfer manager.
119                 *
120                 * <p><b>Note:</b> Calling this method does not automatically wrap the transfer manager.
121                 * It will only be wrapped if the original transfer manager is annotated with the feature
122                 * annotation.
123                 *
124                 * <p>If the requested {@link Feature} is required (as per its definition), but the original
125                 * transfer manager is not annotated with this feature, the creation of the transfer manager
126                 * will fail.
127                 *
128                 * @param featureAnnotation Annotation representing the feature (see features.* package)
129                 * @return Returns this builder class (for more features to be requested)
130                 */
131                public TransferManagerBuilder withFeature(Class<? extends Annotation> featureAnnotation) {
132                        logger.log(Level.INFO, "- With feature " + featureAnnotation.getSimpleName());
133
134                        features.add(featureAnnotation);
135                        return this;
136                }
137
138                /**
139                 * Wraps of the previously requested feature transfer managers and casts the result to the requested class.
140                 * If no specific class is requested, {@link #asDefault()} can be used instead.
141                 *
142                 * @param featureAnnotation Feature annotation corresponding to the requested transfer manager
143                 * @return {@link TransferManager} casted to the feature lasted wrapped (and requested by this method)
144                 */
145                @SuppressWarnings("unchecked")
146                public <T extends TransferManager> T as(Class<? extends Annotation> featureAnnotation) {
147                        Class<T> implementingTransferManagerClass = (Class<T>) getFeatureTransferManagerClass(featureAnnotation);
148                        return wrap(implementingTransferManagerClass);
149                }
150
151                /**
152                 * Wraps of the previously requested feature transfer managers and returns a standard transfer manager.
153                 * @return {@link TransferManager} wrapped with the requested features
154                 */
155                public TransferManager asDefault() {
156                        return wrap(TransferManager.class);
157                }
158
159                private <T extends TransferManager> T wrap(Class<T> desiredTransferManagerClass) {
160                        checkIfAllFeaturesSupported();
161                        checkDuplicateFeatures();
162                        checkRequiredFeatures();
163
164                        applyFeatures();
165
166                        return castToDesiredTransferManager(desiredTransferManagerClass);
167                }
168
169                private void applyFeatures() {
170                        try {
171                                for (Class<? extends Annotation> featureAnnotation : features) {
172                                        boolean isFeatureSupported = ReflectionUtil.isAnnotationPresentInHierarchy(originalTransferManager.getClass(), featureAnnotation);
173
174                                        if (isFeatureSupported) {
175                                                Class<? extends TransferManager> featureTransferManagerClass = getFeatureTransferManagerClass(featureAnnotation);
176                                                wrappedTransferManager = apply(wrappedTransferManager, featureTransferManagerClass, featureAnnotation);
177                                        }
178                                        else {
179                                                logger.log(Level.INFO, "- SKIPPING unsupported optional feature " + featureAnnotation.getSimpleName());
180                                        }
181                                }
182                        }
183                        catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
184                                throw new RuntimeException("Unable to annotate TransferManager with feature.", e);
185                        }
186                }
187
188                private <T extends TransferManager> T castToDesiredTransferManager(Class<T> desiredTransferManagerClass) {
189                        try {
190                                return desiredTransferManagerClass.cast(wrappedTransferManager);
191                        }
192                        catch (ClassCastException e) {
193                                throw new RuntimeException("Unable to wrap TransferManager in " + desiredTransferManagerClass.getSimpleName()
194                                                + " because feature does not seem to be supported", e);
195                        }
196                }
197
198                private TransferManager apply(TransferManager underlyingTransferManager, Class<? extends TransferManager> featureTransferManagerClass,
199                                Class<? extends Annotation> featureAnnotationClass) throws IllegalAccessException, InvocationTargetException, InstantiationException {
200
201                        logger.log(Level.FINE,
202                                        "- Wrapping TransferManager " + underlyingTransferManager.getClass().getSimpleName() + " in " + featureTransferManagerClass.getSimpleName());
203
204                        Annotation concreteFeatureAnnotation = ReflectionUtil.getAnnotationInHierarchy(originalTransferManager.getClass(), featureAnnotationClass);
205                        Constructor<?> transferManagerConstructor = ReflectionUtil.getMatchingConstructorForClass(featureTransferManagerClass, TransferManager.class, TransferManager.class, Config.class, featureAnnotationClass);
206
207                        if (transferManagerConstructor == null) {
208                                throw new RuntimeException("Invalid TransferManager class detected: Unable to find constructor.");
209                        }
210
211                        Annotation featureAnnotation = featureAnnotationClass.cast(concreteFeatureAnnotation);
212                        return (TransferManager) transferManagerConstructor.newInstance(originalTransferManager, underlyingTransferManager, config, featureAnnotation);
213                }
214
215                private static Class<? extends TransferManager> getFeatureTransferManagerClass(Class<? extends Annotation> featureAnnotation) {
216                        String featureTransferManagerClassName = String.format(FEATURE_TRANSFER_MANAGER_FORMAT, featureAnnotation.getSimpleName());
217
218                        try {
219                                return (Class<? extends TransferManager>) Class.forName(featureTransferManagerClassName).asSubclass(TransferManager.class);
220                        }
221                        catch (Exception e) {
222                                throw new RuntimeException("Unable to find class with feature " + featureAnnotation.getSimpleName() + ". Tried " + featureTransferManagerClassName, e);
223                        }
224                }
225
226                private void checkRequiredFeatures() {
227                        // TODO [low] Instead of a feature list, all available @Feature annotations should be listed with reflection
228
229                        for (Class<? extends Annotation> concreteFeatureAnnotation : FEATURE_LIST) {
230                                Feature featureAnnotation = ReflectionUtil.getAnnotationInHierarchy(concreteFeatureAnnotation, Feature.class);
231
232                                if (featureAnnotation.required()) {
233                                        logger.log(Level.FINE, "- Checking required feature " + concreteFeatureAnnotation.getSimpleName() + " in " + originalTransferManager.getClass().getSimpleName() + " ...");
234                                        boolean requiredFeaturePresent = ReflectionUtil.isAnnotationPresentInHierarchy(originalTransferManager.getClass(), concreteFeatureAnnotation);
235
236                                        if (!requiredFeaturePresent) {
237                                                throw new RuntimeException("Required feature " + concreteFeatureAnnotation.getSimpleName() + " is not present in " + originalTransferManager.getClass().getSimpleName());
238                                        }
239                                }
240                        }
241                }
242
243                private void checkDuplicateFeatures() {
244                        logger.log(Level.FINE, "- Checking for duplicate features ...");
245
246                        int listSize = features.size();
247                        int setSize = Sets.newHashSet(features).size();
248
249                        if (listSize != setSize) {
250                                throw new IllegalArgumentException("There are duplicates in feature set: " + features);
251                        }
252                }
253
254                private void checkIfAllFeaturesSupported() {
255                        logger.log(Level.FINE, "- Checking if selected features supported ...");
256
257                        for (Class<? extends Annotation> featureAnnotation : features) {
258                                if (!featureAnnotation.isAnnotationPresent(Feature.class)) {
259                                        throw new IllegalArgumentException("Feature " + featureAnnotation + " is unknown");
260                                }
261                        }
262                }
263        }
264}