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.files;
019
020import java.util.Map;
021import java.util.logging.Level;
022import java.util.logging.Logger;
023
024import org.syncany.plugins.transfer.StorageException;
025import org.syncany.plugins.transfer.TransferManager;
026import org.syncany.util.StringUtil;
027
028import com.google.common.collect.Maps;
029
030/**
031 * A remote file represents a file object on a remote storage. Its purpose is to
032 * identify a file and allow {@link TransferManager}s to upload/download local files.
033 *
034 * <p>Transfer manager operations take either <code>RemoteFile</code> instances, or classes
035 * that extend this class. Depending on the type of the sub-class, they might store the
036 * files at a different location or in a different format to optimize performance.
037 * 
038 * <p>Remote files can be extended with {@link RemoteFileAttributes} in certain situations, 
039 * e.g. to add additional information about the sub-path. The attributes can be added set
040 * and read via {@link #setAttributes(RemoteFileAttributes)} and {@link #getAttributes(Class)}.
041 *
042 * <p><b>Important:</b> Sub-classes must offer a
043 * {@link RemoteFile#RemoteFile(String) one-parameter constructor} that takes a
044 * <code>String</code> argument. This constructor is required by the {@link RemoteFile}.
045 *
046 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
047 */
048public abstract class RemoteFile {
049        private static final Logger logger = Logger.getLogger(RemoteFile.class.getSimpleName());
050
051        private static final String REMOTE_FILE_PACKAGE = RemoteFile.class.getPackage().getName();
052        private static final String REMOTE_FILE_SUFFIX = RemoteFile.class.getSimpleName();
053
054        private String name;
055        private Map<Class<? extends RemoteFileAttributes>, RemoteFileAttributes> attributes;
056        
057        /**
058         * Creates a new remote file by its name. The name is used by {@link TransferManager}s
059         * to identify a file on the remote storage.
060         *
061         * <p>The constructor parses and validates the given name using the
062         * {@link #validateName(String) validateName()} method. While <code>RemoteFile</code> has no name
063         * pattern (and never throws an exception), sub-classes might.
064         *
065         * <p><b>Important:</b> Sub-classes must also implement a one-parameter constructor that takes a
066         * <code>String</code> argument. This constructor is required by the {@link RemoteFile}.
067         *
068         * @param name The name of the file (as it is identified by Syncany)
069         * @throws StorageException If the name does not match the name pattern defined by the class.<br>
070         *         <b>Note:</b> <code>RemoteFile</code> does never throw this exceptions, however, subclasses might.
071         */
072        public RemoteFile(String name) throws StorageException {
073                this.name = validateName(name);
074                this.attributes = Maps.newHashMap();
075        }
076
077        /**
078         * Returns the name of the file (as it is identified by Syncany)
079         */
080        public final String getName() {
081                return name;
082        }
083        
084        /**
085         * Sets remote file attributes to this remote file class. Attributes 
086         * can extend the parameters of this class without actually having to extend it.
087         */
088        public final <T extends RemoteFileAttributes> void setAttributes(T remoteFileAttributes) {
089                attributes.put(remoteFileAttributes.getClass(), remoteFileAttributes);
090        }
091        
092        /**
093         * Returns a list of attributes for a given file, 
094         * or null if there is no attribute with the given class.
095         */
096        @SuppressWarnings("unchecked")
097        public final <T extends RemoteFileAttributes> T getAttributes(Class<T> remoteFileAttributesClass) {
098                return (T) attributes.get(remoteFileAttributesClass);
099        }
100
101        /**
102         * Parses the name of the file and validates it against the classes name pattern. While
103         * <code>RemoteFile</code> has no name pattern (and never throws an exception), sub-classes might by
104         * overriding this method.
105         *
106         * @param name The name of the file (as it is identified by Syncany)
107         * @return Returns a (potentially changed) name, after validating the name
108         * @throws StorageException If the name does not match the name pattern defined by the class.<br>
109         *         <b>Note:</b> <code>RemoteFile</code> does never throw this exceptions, however, subclasses might.
110         */
111        protected String validateName(String name) throws StorageException {
112                return name;
113        }
114
115        /**
116         * Creates a remote file based on a name and a class name.
117         *
118         * <p>The name must match the corresponding name pattern, and the class name
119         * can either be <code>RemoteFile</code>, or a sub-class thereof.
120         *
121         * @param name The name of the remote file
122         * @param remoteFileClass Class name of the object to instantiate, <code>RemoteFile</code> or a sub-class thereof
123         * @return Returns a new object of the given class
124         */
125        public static <T extends RemoteFile> T createRemoteFile(String name, Class<T> remoteFileClass) throws StorageException {
126                try {
127                        return remoteFileClass.getConstructor(String.class).newInstance(name);
128                }
129                catch (Exception e) {
130                        throw new StorageException(e);
131                }
132        }
133
134        /**
135         * Creates a remote file based on a name and derives the class name using the
136         * file name.
137         *
138         * <p>The name must match the corresponding name pattern (nameprefix-...), and
139         * the derived class can either be <code>RemoteFile</code>, or a sub-class thereof.
140         *
141         * @param name The name of the remote file
142         * @return Returns a new object of the given class
143         */
144        @SuppressWarnings("unchecked")
145        public static <T extends RemoteFile> T createRemoteFile(String name) throws StorageException {
146                String prefix = name.contains("-") ? name.substring(0, name.indexOf('-')) : name;
147                String camelCasePrefix = StringUtil.toCamelCase(prefix);
148
149                try {
150                        Class<T> remoteFileClass = (Class<T>) Class.forName(REMOTE_FILE_PACKAGE + "." + camelCasePrefix + REMOTE_FILE_SUFFIX);
151                        return createRemoteFile(name, remoteFileClass);
152                }
153                catch (ClassNotFoundException | StorageException e) {
154                        logger.log(Level.INFO, "Invalid filename for remote file " + name);
155                        throw new StorageException("Invalid filename for remote file " + name);
156                }
157        }
158
159        @Override
160        public boolean equals(Object obj) {
161                if (this == obj) {
162                        return true;
163                }
164
165                if (obj == null || !(obj instanceof RemoteFile)) {
166                        return false;
167                }
168
169                RemoteFile other = (RemoteFile) obj;
170
171                if (name == null) {
172                        if (other.name != null) {
173                                return false;
174                        }
175                }
176                else if (!name.equals(other.name)) {
177                        return false;
178                }
179
180                return true;
181        }
182
183        @Override
184        public int hashCode() {
185                final int prime = 31;
186                int result = 1;
187                result = prime * result + ((name == null) ? 0 : name.hashCode());
188                return result;
189        }
190
191        @Override
192        public String toString() {
193                return RemoteFile.class.getSimpleName() + "[name=" + name + "]";
194        }
195}