/* * Copyright 2004 - 2008 Christian Sprajc. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder 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. * * PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id$ */ package de.dal33t.powerfolder.disk; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Logger; import de.dal33t.powerfolder.ConfigurationEntry; import de.dal33t.powerfolder.Controller; import de.dal33t.powerfolder.message.Invitation; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.Translation; /** * Instance of this class describe how a folder should be synchronized with the * remote sides. Profiles are shared within PowerFolder. There should never be * two profiles with identical configurations or name. Thus if a folder has a * particular profile, and that profile is edited, all other folders that have * the same profile are directly affected. It is illegal to set a profile's * profileName the same as another profile. SyncProfile maintains two static * caches, one for preset (non-editable) profiles, and one for custom profiles. * The preset profiles and the preset cache can not be modified. Both caches are * protected inside this class and are not intended for direct external * modification. Access to the custom caches should always be synchronized. * SyncProfile has no public constructor. Custom profiles can be created using * the retrieveSyncProfile() method. These will first try to find from the * caches a profile with the same internal configuration as requested. Failing * this a new (custom) profile will be created and is added to the custom cache. * Note that if a matching profile is found in one of the caches, the * profileName of the returned profile will probably not equal the * profileNameArg supplied. Profiles can be saved and loaded as comma-separated * lists. Note that the old way to store a profile was as a simple 'profile id'. * getSyncProfileByFieldList() still supports this method for backward * compatability. If a profile is loaded from the config file and has the same * name but different internal configuration as another profile already in one * of the caches, then it is given an auto-generated name by adding a unique * number to the profileName, like 'Custom profile 3'. Preset profiles always * get their name from an id. This ensures that the true translated name is * showen if the language is changed (restart). Do not serialize SyncProfiles. * They will not be accepted into the caches on the target system when * deserialized. Use getFieldList() to transfer. This implements Serializable * ONLY for compliance with old Invitations. * * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a> * @version $Revision: 1.5 $ * @see Invitation */ public class SyncProfile implements Serializable { private static final long serialVersionUID = 100L; /** Field delimiter for field list */ public static final String FIELD_LIST_DELIMITER = ","; /** * Host files preset profile. */ public static final SyncProfile HOST_FILES = new SyncProfile("host_files", false, new SyncProfileConfiguration(false, false, false, false, 30, false, 12, 1, SyncProfileConfiguration.REGULAR_TIME_TYPE_MINUTES, true)); /** * Automatic download preset profile. */ public static final SyncProfile AUTOMATIC_DOWNLOAD = new SyncProfile( "automatic_download", false, new SyncProfileConfiguration(true, true, false, false, 30, false, 12, 1, SyncProfileConfiguration.REGULAR_TIME_TYPE_MINUTES, true)); /** * Automatic synchronization preset profile. Uses JNotify to do instant. * sync. */ public static final SyncProfile AUTOMATIC_SYNCHRONIZATION = new SyncProfile( "auto_sync", false, new SyncProfileConfiguration(true, true, true, true, 1, false, 12, 1, SyncProfileConfiguration.REGULAR_TIME_TYPE_MINUTES, true)); /** * Backup source preset profile. Uses JNotify to do instant. */ public static final SyncProfile BACKUP_SOURCE = new SyncProfile( "backup_source", false, new SyncProfileConfiguration(false, false, false, false, 1, false, 12, 1, SyncProfileConfiguration.REGULAR_TIME_TYPE_MINUTES, true)); /** * Backup target preset profile. */ public static final SyncProfile BACKUP_TARGET = new SyncProfile( "backup_target", false, new SyncProfileConfiguration(true, true, true, true, 60)); /** * Manual synchronization preset profile. */ public static final SyncProfile MANUAL_SYNCHRONIZATION = new SyncProfile( "manual_synchronization", false, new SyncProfileConfiguration(false, false, false, false, 0)); // All preset sync profiles private static final SyncProfile[] PRESET_SYNC_PROFILES = { AUTOMATIC_SYNCHRONIZATION, MANUAL_SYNCHRONIZATION, BACKUP_SOURCE, BACKUP_TARGET, AUTOMATIC_DOWNLOAD, HOST_FILES}; /** Special no-sync profile for preview folders. Same config as PROJECT_WORK */ public static final SyncProfile NO_SYNC = new SyncProfile("no_sync", false, new SyncProfileConfiguration(false, false, false, false, 0)); /** * Special no-sync profile for disabled folders in Online Storage. Only * syncs file deletions */ public static final SyncProfile DISABLED = new SyncProfile("disabled", false, new SyncProfileConfiguration(false, false, true, true, 0)); /** * Backup source preset profile for Online Storage. Don't scan for local * changes */ public static final SyncProfile BACKUP_TARGET_NO_CHANGE_DETECT = new SyncProfile( "backup_target_no_change", false, new SyncProfileConfiguration(true, true, true, true, 0)); /** * Especially for meta-folders. Scan every minute. */ public static SyncProfile META_FOLDER_SYNC = new SyncProfile( "backup_target_no_change", false, new SyncProfileConfiguration(true, true, true, true, 1)); /** * All custom profiles */ private static final List<SyncProfile> customProfiles = new ArrayList<SyncProfile>(); /** * The name of the profile (for custom profiles) */ private String profileName; /** * The id of the profile (for preset profiles) */ private String profileId; /** * Indicates that this is a custom profile. This should only ever be false * for the static preset profiles created inside this class. */ private final boolean custom; /** * The internal configuration of the profile. This determines how a folder * synchronizes with other nodes. */ private SyncProfileConfiguration configuration; /** * Constructor. * * @param profileNameId * name (custom) or id (preset) of the profile * @param custom * whether this is a custom profile * @param configuration * the configuration of the profile */ private SyncProfile(String profileNameId, boolean custom, SyncProfileConfiguration configuration) { if (custom) { profileName = profileNameId; } else { profileId = profileNameId; } this.custom = custom; this.configuration = configuration; } /** * Returns tue if this is a custom profile. * * @return */ public boolean isCustom() { return custom; } /** * @return the profile name. */ public String getName() { if (custom) { return profileName; } else { return translateId(profileId); } } /** * Sets the profile name. It is illegal to set the profileName the same as * another profile, because this breaks the required uniquness of profiles. * Always test for name uniqueness first with the safe checkName() method. * * @param profileName */ public void setName(String profileName) { Reject.ifFalse(custom, "Cannot set the profileName of preset profile " + getName() + " to " + profileName); Reject.ifBlank(profileName, "ProfileName not supplied"); // Ensure that the name is not being set to an existing sync profile // name for (SyncProfile profile : PRESET_SYNC_PROFILES) { if (!equals(profile) && profile.getName().equals(profileName)) { throw new RuntimeException( "Preset profile name already exists."); } } synchronized (customProfiles) { for (SyncProfile customProfile : customProfiles) { if (!equals(customProfile) && customProfile.getName().equals(profileName)) { throw new RuntimeException( "Custom profile name already exists."); } } } this.profileName = profileName; } public SyncProfileConfiguration getConfiguration() { return configuration; } public void setConfiguration(SyncProfileConfiguration configuration) { Reject.ifFalse(custom, "Cannot set the configuration of preset profile " + getName()); Reject.ifNull(configuration, "configuration not supplied"); // Ensure that the config is unique for (SyncProfile profile : PRESET_SYNC_PROFILES) { if (!equals(profile) && profile.configuration.equals(configuration)) { throw new RuntimeException( "Preset profile config already exists."); } } synchronized (customProfiles) { for (SyncProfile customProfile : customProfiles) { if (!equals(customProfile) && customProfile.configuration.equals(configuration)) { throw new RuntimeException( "Custom profile config already exists."); } } } this.configuration = configuration; } /** * This is used to persist profiles to the configuration. NOTE: Existing * sync profiles may not load if this is changed. Add any new fields to the * end of the list. * * @return string representation of the profile config as a list of fields */ public String getFieldList() { // Twice for backward compatibility. TRAC #1626 return configuration.isAutoDownload() + FIELD_LIST_DELIMITER + configuration.isAutoDownload() + FIELD_LIST_DELIMITER // Twice for backward compatibility. TRAC #1626 + configuration.isSyncDeletion() + FIELD_LIST_DELIMITER + configuration.isSyncDeletion() + FIELD_LIST_DELIMITER + configuration.getTimeBetweenRegularScans() + FIELD_LIST_DELIMITER + configuration.isDailySync() + FIELD_LIST_DELIMITER + configuration.getDailyHour() + FIELD_LIST_DELIMITER + configuration.getDailyDay() + FIELD_LIST_DELIMITER + configuration.getRegularTimeType() + FIELD_LIST_DELIMITER + getName() + FIELD_LIST_DELIMITER + configuration.isInstantSync(); } /** * For preset config, the name is i18n using 'transfer_mode.x.name'. * * @param id * translate 'syncprofile.[id].name' * @return */ private static String translateId(String id) { return Translation.getTranslation("transfer_mode." + id + ".name"); } /** * Method for either retrieving or creating a sync profile from the caches. * Note that if a profile is retrieved, it may not have the same name as the * profileNameArg arg, but it will have the same configuration. */ public static SyncProfile retrieveSyncProfile(String profileNameArg, SyncProfileConfiguration syncProfileConfigurationArg) { Reject.ifNull(syncProfileConfigurationArg, "Null sync profile configuration"); List<String> names = new ArrayList<String>(); // Check presetProfiles for (SyncProfile profile : PRESET_SYNC_PROFILES) { if (profile.configuration.equals(syncProfileConfigurationArg)) { return profile; } names.add(profile.getName()); } // Check existing profiles synchronized (customProfiles) { for (SyncProfile customProfile : customProfiles) { if (customProfile.configuration .equals(syncProfileConfigurationArg)) { return customProfile; } names.add(customProfile.getName()); } } // Ensure new profile has a unique name; boolean emptyName = profileNameArg.trim().length() == 0; String workingProfileName = emptyName ? translateId("custom") : profileNameArg; SyncProfile syncProfile; if (names.contains(workingProfileName) || emptyName) { int i = 1; while (names.contains(workingProfileName + ' ' + i)) { i++; } syncProfile = new SyncProfile(workingProfileName + ' ' + i, true, syncProfileConfigurationArg); } else { syncProfile = new SyncProfile(workingProfileName, true, syncProfileConfigurationArg); } // Store in the custom cache. synchronized (customProfiles) { customProfiles.add(syncProfile); } return syncProfile; } /** * Gets a copy of the sync profiles. Adding or deleting from this list does * not affect the SyncProfile caches, but changing the profile config does. * * @return Shallow copy of SyncProfile caches. */ public static List<SyncProfile> getSyncProfilesCopy() { List<SyncProfile> list = new ArrayList<SyncProfile>(); list.addAll(Arrays.asList(PRESET_SYNC_PROFILES)); synchronized (customProfiles) { for (SyncProfile customProfile : customProfiles) { if (!list.contains(customProfile)) { list.add(customProfile); } } } return list; } /** * Tries to resolve a sync profile by id (the old way of storing sync * profiles). Else it expects a comma-separated list of profile fieldList. * * @param fieldList * @return * @see #getFieldList() */ public static SyncProfile getSyncProfileByFieldList(String fieldList) { Reject.ifNull(fieldList, "Null sync profile fieldList"); // Old way was to store the SyncProfile's id. search presets if (!fieldList.contains(FIELD_LIST_DELIMITER)) { for (SyncProfile syncProfile : PRESET_SYNC_PROFILES) { if (fieldList.equals(syncProfile.profileId)) { return syncProfile; } } } // Preferred way is to store the sync profile as its getFieldList(). // This allows for custom profiles. StringTokenizer st = new StringTokenizer(fieldList, FIELD_LIST_DELIMITER); boolean autoDownloadFromFriends = false; if (st.hasMoreTokens()) { autoDownloadFromFriends = Boolean.parseBoolean(st.nextToken()); } boolean autoDownloadFromOthers = false; if (st.hasMoreTokens()) { autoDownloadFromOthers = Boolean.parseBoolean(st.nextToken()); } boolean syncDeletionWithFriends = false; if (st.hasMoreTokens()) { syncDeletionWithFriends = Boolean.parseBoolean(st.nextToken()); } boolean syncDeletionWithOthers = false; if (st.hasMoreTokens()) { syncDeletionWithOthers = Boolean.parseBoolean(st.nextToken()); } int timeBetweenScans = 0; if (st.hasMoreTokens()) { timeBetweenScans = Integer.parseInt(st.nextToken()); } boolean dailySync = false; if (st.hasMoreTokens()) { dailySync = Boolean.parseBoolean(st.nextToken()); } int dailyHour = SyncProfileConfiguration.DAILY_HOUR_DEFAULT; if (st.hasMoreTokens()) { dailyHour = Integer.parseInt(st.nextToken()); } int dailyDay = SyncProfileConfiguration.DAILY_DAY_EVERY_DAY; if (st.hasMoreTokens()) { dailyDay = Integer.parseInt(st.nextToken()); } String timeType = SyncProfileConfiguration.REGULAR_TIME_TYPE_MINUTES; if (st.hasMoreTokens()) { timeType = st.nextToken(); } String profileName = ""; if (st.hasMoreTokens()) { profileName = st.nextToken(); } boolean instantSync = false; if (st.hasMoreTokens()) { instantSync = Boolean.parseBoolean(st.nextToken()); } return retrieveSyncProfile(profileName, new SyncProfileConfiguration( autoDownloadFromFriends, autoDownloadFromOthers, syncDeletionWithFriends, syncDeletionWithOthers, timeBetweenScans, dailySync, dailyHour, dailyDay, timeType, instantSync)); } /** * #2132 * * @param controller * @return the default sync profile according to the configuration. */ public static SyncProfile getDefault(Controller controller) { String defFieldList = ConfigurationEntry.DEFAULT_TRANSFER_MODE .getValue(controller); try { return SyncProfile.getSyncProfileByFieldList(defFieldList); } catch (Exception e) { Logger.getLogger(SyncProfile.class.getName()).severe( "Unable to get default transfer mode for " + defFieldList + ". Please check your config: " + e); return SyncProfile .getSyncProfileByFieldList(ConfigurationEntry.DEFAULT_TRANSFER_MODE .getDefaultValue()); } } /** * @return true if folder automatically detects changes to files on disk */ public boolean isInstantSync() { return configuration.isInstantSync(); } /** * @return true if folder detects changes in periodic timeframes (e.g. every * hour). */ public boolean isPeriodicSync() { return configuration.isPeriodicSync(); } /** * @return true if this profile only detects changes when user presses * manually the sync button. */ public boolean isManualSync() { return configuration.isManualSync(); } /** * @return true if folder detects changes in daily/scheduled timeframes * (e.g. Mo / 1200 pm). */ public boolean isDailySync() { return configuration.isDailySync(); } /** * Answers the seconds to wait between disk scans. Only relevant if * auto-detect changes is enabled * * @return */ public int getSecondsBetweenScans() { String timeType = configuration.getRegularTimeType(); if (configuration.getRegularTimeType() == null) { timeType = SyncProfileConfiguration.REGULAR_TIME_TYPE_MINUTES; } if (SyncProfileConfiguration.REGULAR_TIME_TYPE_SECONDS.equals(timeType)) { return configuration.getTimeBetweenRegularScans(); } else if (SyncProfileConfiguration.REGULAR_TIME_TYPE_HOURS .equals(timeType)) { return configuration.getTimeBetweenRegularScans() * 3600; } else { return configuration.getTimeBetweenRegularScans() * 60; } } /** * @return true if new/update files should be automatically downloaded; */ public boolean isAutodownload() { return configuration.isAutoDownload(); } /** * @return true if syncing deletions with any other user */ public boolean isSyncDeletion() { return configuration.isSyncDeletion(); } /** * Remove a profile from the cache. * * @param profileArg */ public static void deleteProfile(SyncProfile profileArg) { synchronized (customProfiles) { for (Iterator<SyncProfile> iter = customProfiles.iterator(); iter .hasNext();) { SyncProfile profile = iter.next(); if (profile.equals(profileArg)) { iter.remove(); } } } } /** * Check equality on configuration only. This is the important field. * * @param obj * @return */ public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } SyncProfile that = (SyncProfile) obj; return configuration.equals(that.configuration); } /** * Like equal. * * @return */ public int hashCode() { return configuration.hashCode(); } }