/* * Jajuk * Copyright (C) The Jajuk Team * http://jajuk.info * * 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 2 * of the License, or 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ package org.jajuk.base; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.ImageIcon; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.apache.commons.lang.StringUtils; import org.jajuk.events.JajukEvent; import org.jajuk.events.JajukEvents; import org.jajuk.events.ObservationManager; import org.jajuk.services.bookmark.History; import org.jajuk.services.core.ExitService; import org.jajuk.services.players.QueueModel; import org.jajuk.ui.helpers.ManualDeviceRefreshReporter; import org.jajuk.ui.helpers.RefreshReporter; import org.jajuk.ui.widgets.InformationJPanel; import org.jajuk.ui.windows.JajukMainWindow; import org.jajuk.util.Conf; import org.jajuk.util.Const; import org.jajuk.util.IconLoader; import org.jajuk.util.JajukFileFilter; import org.jajuk.util.JajukIcons; import org.jajuk.util.Messages; import org.jajuk.util.UtilGUI; import org.jajuk.util.UtilString; import org.jajuk.util.UtilSystem; import org.jajuk.util.error.JajukException; import org.jajuk.util.filters.ImageFilter; import org.jajuk.util.filters.KnownTypeFilter; import org.jajuk.util.log.Log; import org.xml.sax.Attributes; /** * A device ( music files repository ) * <p> * Some properties of a device are immutable : name, url and type * * <p> * Physical item. */ public class Device extends PhysicalItem implements Comparable<Device> { /** The Constant OPTION_REFRESH_DEEP.*/ private static final int OPTION_REFRESH_DEEP = 1; /** The Constant OPTION_REFRESH_CANCEL. */ private static final int OPTION_REFRESH_CANCEL = 2; // Device type constants /** * . */ public enum Type { DIRECTORY, FILES_CD, NETWORK_DRIVE, EXTDD, PLAYER } /** Device URL (performances). */ private String sUrl; /** IO file for optimizations*. */ private java.io.File fio; /** Mounted device flag. */ private boolean bMounted = false; /** directories. */ private final List<Directory> alDirectories = new ArrayList<Directory>(20); /** Already refreshing flag. */ private volatile boolean bAlreadyRefreshing = false; //NOSONAR /** Already synchronizing flag. */ private volatile boolean bAlreadySynchronizing = false; //NOSONAR /** Volume of created files during synchronization. */ private long lVolume = 0; /** date last refresh. */ private long lDateLastRefresh; /** Progress reporter *. */ private RefreshReporter reporter; /** Refresh deepness choice *. */ private int choice = Device.OPTION_REFRESH_DEEP; /** [PERF] cache rootDir directory. */ private Directory rootDir; /** * Device constructor. * * @param sId * @param sName */ Device(final String sId, final String sName) { super(sId, sName); } /** * Adds the directory. * * @param directory */ void addDirectory(final Directory directory) { alDirectories.add(directory); } /** * Scan directories to cleanup removed files and playlists. * * @param dirsToRefresh list of the directory to refresh, null if all of them * * @return whether some items have been removed */ public boolean cleanRemovedFiles(List<Directory> dirsToRefresh) { long l = System.currentTimeMillis(); // directories cleanup boolean bChanges = cleanDirectories(dirsToRefresh); // files cleanup bChanges = bChanges | cleanFiles(dirsToRefresh); // Playlist cleanup bChanges = bChanges | cleanPlaylist(dirsToRefresh); // clear history to remove old files referenced in it if (Conf.getString(Const.CONF_HISTORY) != null) { History.getInstance().clear(Integer.parseInt(Conf.getString(Const.CONF_HISTORY))); } // delete old history items l = System.currentTimeMillis() - l; Log.debug("{{" + getName() + "}} Old file references cleaned in: " + ((l < 1000) ? l + " ms, changes: " + bChanges : l / 1000 + " s, changes: " + bChanges)); return bChanges; } /** * Walk through all Playlists and remove the ones for the current device. * * @param dirsToRefresh list of the directory to refresh, null if all of them * * @return true if there was any playlist removed */ private boolean cleanPlaylist(List<Directory> dirsToRefresh) { boolean bChanges = false; final List<Playlist> plfiles = PlaylistManager.getInstance().getPlaylists(); for (final Playlist plf : plfiles) { // check if it is a playlist located inside refreshed directory if (dirsToRefresh != null) { boolean checkIt = false; for (Directory directory : dirsToRefresh) { if (plf.hasAncestor(directory)) { checkIt = true; } } // This item is not in given directories, just continue if (!checkIt) { continue; } } if (!ExitService.isExiting() && plf.getDirectory().getDevice().equals(this) && plf.isReady() && !plf.getFIO().exists()) { PlaylistManager.getInstance().removeItem(plf); Log.debug("Removed: " + plf); if (reporter != null) { reporter.notifyFileOrPlaylistDropped(); } bChanges = true; } } return bChanges; } /** * Walk through tall Files and remove the ones for the current device. * * @param dirsToRefresh list of the directory to refresh, null if all of them * * @return true if there was any file removed. */ private boolean cleanFiles(List<Directory> dirsToRefresh) { boolean bChanges = false; final List<org.jajuk.base.File> files = FileManager.getInstance().getFiles(); for (final org.jajuk.base.File file : files) { // check if it is a file located inside refreshed directory if (dirsToRefresh != null) { boolean checkIt = false; for (Directory directory : dirsToRefresh) { if (file.hasAncestor(directory)) { checkIt = true; } } // This item is not in given directories, just continue if (!checkIt) { continue; } } if (!ExitService.isExiting() && file.getDirectory().getDevice().equals(this) && file.isReady() && // Remove file if it doesn't exist any more or if it is a iTunes // file (useful for jajuk < 1.4) (!file.getFIO().exists() || file.getName().startsWith("._"))) { FileManager.getInstance().removeFile(file); Log.debug("Removed: " + file); bChanges = true; if (reporter != null) { reporter.notifyFileOrPlaylistDropped(); } } } return bChanges; } /** * Walks through all directories and removes the ones for this device. * * @param dirsToRefresh list of the directory to refresh, null if all of them * * @return true if there was any directory removed */ private boolean cleanDirectories(List<Directory> dirsToRefresh) { boolean bChanges = false; List<Directory> dirs = null; if (dirsToRefresh == null) { dirs = DirectoryManager.getInstance().getDirectories(); } else { // If one or more named directories are provided, not only clean them up but also their sub directories dirs = new ArrayList<Directory>(dirsToRefresh); for (Directory dir : dirsToRefresh) { dirs.addAll(dir.getDirectoriesRecursively()); } } for (final Directory dir : dirs) { if (!ExitService.isExiting() && dir.getDevice().equals(this) && dir.getDevice().isMounted() && !dir.getFio().exists()) { // note that associated files are removed too DirectoryManager.getInstance().removeDirectory(dir.getID()); Log.debug("Removed: " + dir); bChanges = true; } } return bChanges; } /** * Alphabetical comparator used to display ordered lists of devices. * * @param otherDevice * * @return comparison result */ @Override public int compareTo(final Device otherDevice) { // should handle null if (otherDevice == null) { return -1; } // We must be consistent with equals, see // http://java.sun.com/javase/6/docs/api/java/lang/Comparable.html int comp = getName().compareToIgnoreCase(otherDevice.getName()); if (comp == 0) { return getName().compareTo(otherDevice.getName()); } else { return comp; } } /** * Gets the date last refresh. * * @return the date last refresh */ public long getDateLastRefresh() { return lDateLastRefresh; } /* (non-Javadoc) * @see org.jajuk.base.Item#getTitle() */ @Override public String getTitle() { return Messages.getString("Item_Device") + " : " + getName(); } /** * Gets the device type as a string. * * @return the device type as string */ public String getDeviceTypeS() { return getType().name(); } /** * Gets the directories directly under the device root (not recursive). * * @return the directories */ public List<Directory> getDirectories() { return alDirectories; } /** * return ordered child files recursively. * * @return child files recursively */ public List<org.jajuk.base.File> getFilesRecursively() { // looks for the root directory for this device Directory dirRoot = getRootDirectory(); if (dirRoot != null) { return dirRoot.getFilesRecursively(); } // nothing found, return empty list return new ArrayList<org.jajuk.base.File>(); } /** * Gets the fio. * * @return Returns the IO file reference to this directory. */ public File getFIO() { return fio; } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getHumanValue(java.lang.String) */ @Override public String getHumanValue(final String sKey) { if (Const.XML_TYPE.equals(sKey)) { return getTypeLabel(getType()); } else {// default return super.getHumanValue(sKey); } } /** * Return label for a type. * * @param type * @return label for a type */ public static String getTypeLabel(Type type) { if (type == Type.DIRECTORY) { return Messages.getString("Device_type.directory"); } else if (type == Type.FILES_CD) { return Messages.getString("Device_type.file_cd"); } else if (type == Type.EXTDD) { return Messages.getString("Device_type.extdd"); } else if (type == Type.PLAYER) { return Messages.getString("Device_type.player"); } else if (type == Type.NETWORK_DRIVE) { return Messages.getString("Device_type.network_drive"); } else { return null; } } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getIconRepresentation() */ @Override public ImageIcon getIconRepresentation() { if (getType() == Type.DIRECTORY) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_MOUNTED_SMALL), IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_UNMOUNTED_SMALL)); } else if (getType() == Type.FILES_CD) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_CD_MOUNTED_SMALL), IconLoader.getIcon(JajukIcons.DEVICE_CD_UNMOUNTED_SMALL)); } else if (getType() == Type.NETWORK_DRIVE) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_MOUNTED_SMALL), IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_UNMOUNTED_SMALL)); } else if (getType() == Type.EXTDD) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_MOUNTED_SMALL), IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_UNMOUNTED_SMALL)); } else if (getType() == Type.PLAYER) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_MOUNTED_SMALL), IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_UNMOUNTED_SMALL)); } else { Log.warn("Unknown type of device detected: " + getType().name()); return null; } } /* * Return large icon representation of the device * @Return large icon representation of the device */ /** * Gets the icon representation large. * * @return the icon representation large */ public ImageIcon getIconRepresentationLarge() { if (getType() == Type.DIRECTORY) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_MOUNTED), IconLoader.getIcon(JajukIcons.DEVICE_DIRECTORY_UNMOUNTED)); } else if (getType() == Type.FILES_CD) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_CD_MOUNTED), IconLoader.getIcon(JajukIcons.DEVICE_CD_UNMOUNTED)); } else if (getType() == Type.NETWORK_DRIVE) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_MOUNTED), IconLoader.getIcon(JajukIcons.DEVICE_NETWORK_DRIVE_UNMOUNTED)); } else if (getType() == Type.EXTDD) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_MOUNTED), IconLoader.getIcon(JajukIcons.DEVICE_EXT_DD_UNMOUNTED)); } else if (getType() == Type.PLAYER) { return rightIcon(IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_MOUNTED), IconLoader.getIcon(JajukIcons.DEVICE_PLAYER_UNMOUNTED)); } else { Log.warn("Unknown type of device detected: " + getType().name()); return null; } } /** * Return the right icon between mounted or unmounted. * * @param mountedIcon The icon to return for a mounted device * @param unmountedIcon The icon to return for an unmounted device * @return Returns either of the two provided icons depending on the state of * the device */ private ImageIcon rightIcon(ImageIcon mountedIcon, ImageIcon unmountedIcon) { if (isMounted()) { return mountedIcon; } else { return unmountedIcon; } } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getIdentifier() */ @Override public final String getXMLTag() { return Const.XML_DEVICE; } /** * Gets the root directory. * * @return Associated root directory */ public Directory getRootDirectory() { if (rootDir == null) { rootDir = DirectoryManager.getInstance().getDirectoryForIO(getFIO(), this); } return rootDir; } /** * Gets the type. * * @return the type */ public Device.Type getType() { return Type.values()[(int) getLongValue(Const.XML_TYPE)]; } /** * Gets the url. * * @return the url */ public String getUrl() { return sUrl; } /** * Checks if is mounted. * * @return true, if is mounted */ public boolean isMounted() { return bMounted; } /** * Return true if the device can be accessed right now. * * @return true the file can be accessed right now */ public boolean isReady() { if (isMounted() && !isRefreshing() && !isSynchronizing()) { return true; } return false; } /** * Tells if a device is refreshing. * * @return true, if checks if is refreshing */ public boolean isRefreshing() { return bAlreadyRefreshing; } /** * Tells if a device is synchronizing. * * @return true, if checks if is synchronizing */ public boolean isSynchronizing() { return bAlreadySynchronizing; } /** * Manual refresh, displays a dialog. * * @param bAsk ask for refreshing type (deep or fast ?) * @param bAfterMove is this refresh done after a device location change ? * @param forcedDeep : override bAsk and force a deep refresh * @param dirsToRefresh : only refresh specified dirs, or all of them if null */ public void manualRefresh(final boolean bAsk, final boolean bAfterMove, final boolean forcedDeep, List<Directory> dirsToRefresh) { int i = 0; try { i = prepareRefresh(bAsk); if (i == OPTION_REFRESH_CANCEL) { return; } bAlreadyRefreshing = true; } catch (JajukException je) { Messages.showErrorMessage(je.getCode()); Log.debug(je); return; } try { reporter = new ManualDeviceRefreshReporter(this); reporter.startup(); // clean old files up (takes a while) if (!bAfterMove) { cleanRemovedFiles(dirsToRefresh); } reporter.cleanupDone(); // Actual refresh refreshCommand(((i == Device.OPTION_REFRESH_DEEP) || forcedDeep), true, dirsToRefresh); // cleanup logical items org.jajuk.base.Collection.cleanupLogical(); // if it is a move, clean old files *after* the refresh if (bAfterMove) { cleanRemovedFiles(dirsToRefresh); } // notify views to refresh ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_REFRESH)); } finally { // Do not let current reporter as a manual reporter because it would fail // in NPE with auto-refresh reporter = null; // Make sure to unlock refreshing bAlreadyRefreshing = false; } } /** * Prepare manual refresh. * * @param bAsk ask user to perform deep or fast refresh * * @return the user choice (deep or fast) * * @throws JajukException if user canceled, device cannot be refreshed or device already * refreshing */ int prepareRefresh(final boolean bAsk) throws JajukException { if (bAsk) { final Object[] possibleValues = { Messages.getString("FilesTreeView.60"),// fast Messages.getString("FilesTreeView.61"),// deep Messages.getString("Cancel") };// cancel try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { choice = JOptionPane.showOptionDialog(JajukMainWindow.getInstance(), Messages.getString("FilesTreeView.59"), Messages.getString("Option"), JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, possibleValues, possibleValues[0]); } }); } catch (Exception e) { Log.error(e); choice = Device.OPTION_REFRESH_CANCEL; } if (choice == Device.OPTION_REFRESH_CANCEL) { // Cancel return choice; } } // JajukException are not trapped, will be thrown to the caller final Device device = this; if (!device.isMounted()) { // Leave if user canceled device mounting if (!device.mount(true)) { return Device.OPTION_REFRESH_CANCEL; } } if (bAlreadyRefreshing) { throw new JajukException(107); } return choice; } /** * Check that the device is available and not void. * <p>We Cannot mount void devices because of the jajuk reference cleanup thread * ( a refresh would clear the entire device collection)</p> * * @return true if the device is ready for mounting, false if the device is void * */ private boolean checkDevice() { return pathExists() && !isVoid(); } /** * Return whether a device maps a void directory. * * @return whether a device maps a void directory */ private boolean isVoid() { final File file = new File(getUrl()); return (file.listFiles() == null || file.listFiles().length == 0); } /** * Return whether the device path exists at this time. * * @return whether the device path exists at this time */ private boolean pathExists() { final File file = new File(getUrl()); return file.exists(); } /** * Mount the device. * * @param bManual set whether mount is manual or auto * * @return whether the device has been mounted. If user is asked for mounting but cancel, this method returns false. * * @throws JajukException if device cannot be mounted due to technical reason. */ public boolean mount(final boolean bManual) throws JajukException { if (bMounted) { // Device already mounted throw new JajukException(111); } // Check if we can mount the device. boolean readyToMount = checkDevice(); // Effective mounting if available. if (readyToMount) { bMounted = true; } else if (pathExists() && isVoid() && bManual) { // If the device is void and in manual mode, leave a chance to the user to // force it final int answer = Messages.getChoice( "[" + getName() + "] " + Messages.getString("Confirmation_void_refresh"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); // leave if user doesn't confirm to mount the void device if (answer != JOptionPane.YES_OPTION) { return false; } else { bMounted = true; } } else { throw new JajukException(11, "\"" + getName() + "\" at URL : " + getUrl()); } // notify views to refresh if needed ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_MOUNT)); return bMounted; } /** * Set all personal properties of an XML file for an item (doesn't overwrite * existing properties for perfs). * * @param attributes : * list of attributes for this XML item */ @Override public void populateProperties(final Attributes attributes) { for (int i = 0; i < attributes.getLength(); i++) { final String sProperty = attributes.getQName(i); if (!getProperties().containsKey(sProperty)) { String sValue = attributes.getValue(i); final PropertyMetaInformation meta = getMeta(sProperty); // compatibility code for <1.1 : auto-refresh is now a double, // no more a boolean if (meta.getName().equals(Const.XML_DEVICE_AUTO_REFRESH) && (sValue.equalsIgnoreCase(Const.TRUE) || sValue.equalsIgnoreCase(Const.FALSE))) { if (getType() == Type.DIRECTORY) { sValue = "0.5d"; } if (getType() == Type.FILES_CD) { sValue = "0d"; } if (getType() == Type.NETWORK_DRIVE) { sValue = "0d"; } if (getType() == Type.EXTDD) { sValue = "3d"; } if (getType() == Type.PLAYER) { sValue = "3d"; } } try { setProperty(sProperty, UtilString.parse(sValue, meta.getType())); } catch (final Exception e) { Log.error(137, sProperty, e); } } } } /** * Refresh : scan the device to find tracks. * This method is only called from GUI. auto-refresh uses refreshCommand() directly. * * @param bAsynchronous : * set asynchronous or synchronous mode * @param bAsk whether we ask for fast/deep scan * @param bAfterMove whether this is called after a device move * @param dirsToRefresh : only refresh specified dirs, or all of them if null */ public void refresh(final boolean bAsynchronous, final boolean bAsk, final boolean bAfterMove, final List<Directory> dirsToRefresh) { if (bAsynchronous) { final Thread t = new Thread("Device Refresh Thread for : " + name) { @Override public void run() { manualRefresh(bAsk, bAfterMove, false, dirsToRefresh); } }; t.setPriority(Thread.MIN_PRIORITY); t.start(); } else { manualRefresh(bAsk, bAfterMove, false, dirsToRefresh); } } /** * Deep / full Refresh with GUI. */ public void manualRefreshDeep() { final Thread t = new Thread("Device Deep Refresh Thread for : " + name) { @Override public void run() { manualRefresh(false, false, true, null); } }; t.setPriority(Thread.MIN_PRIORITY); t.start(); } /** * The refresh itself. * * @param bDeepScan whether it is a deep refresh request or only fast * @param bManual whether it is a manual refresh or auto * @param dirsToRefresh list of the directory to refresh, null if all of them * * @return true if some changes occurred in device */ boolean refreshCommand(final boolean bDeepScan, final boolean bManual, List<Directory> dirsToRefresh) { try { // Check if this device is mounted (useful when called by // automatic refresh) if (!isMounted()) { return false; } // Check that device is still available boolean readyToMount = checkDevice(); if (!readyToMount) { return false; } bAlreadyRefreshing = true; // reporter is already set in case of manual refresh if (reporter == null) { reporter = new RefreshReporter(this); } // Notify the reporter of the actual refresh startup reporter.refreshStarted(); lDateLastRefresh = System.currentTimeMillis(); // check Jajuk is not exiting because a refresh cannot start in // this state if (ExitService.isExiting()) { return false; } int iNbFilesBeforeRefresh = FileManager.getInstance().getElementCount(); int iNbDirsBeforeRefresh = DirectoryManager.getInstance().getElementCount(); int iNbPlaylistsBeforeRefresh = PlaylistManager.getInstance().getElementCount(); if (bDeepScan && Log.isDebugEnabled()) { Log.debug("Starting refresh of device : " + this); } // Create a directory for device itself and scan files to allow // files at the root of the device final Directory top = DirectoryManager.getInstance().registerDirectory(this); if (!getDirectories().contains(top)) { addDirectory(top); } // Start actual scan List<Directory> dirs = null; if (dirsToRefresh == null) { // No directory specified ? refresh the top directory dirs = new ArrayList<Directory>(1); dirs.add(top); } else { dirs = dirsToRefresh; } for (Directory dir : dirs) { scanRecursively(dir, bDeepScan); } // Force a GUI refresh if new files or directories discovered or have been // removed if (((FileManager.getInstance().getElementCount() - iNbFilesBeforeRefresh) != 0) || ((DirectoryManager.getInstance().getElementCount() - iNbDirsBeforeRefresh) != 0) || ((PlaylistManager.getInstance().getElementCount() - iNbPlaylistsBeforeRefresh) != 0)) { return true; } return false; } catch (final Exception e) { // and regular ones logged Log.error(e); return false; } finally { // make sure to unlock refreshing even if an error occurred bAlreadyRefreshing = false; // reporter is null if mount is not mounted due to early return if (reporter != null) { // Notify the reporter of the actual refresh startup reporter.done(); // Reset the reporter as next time, it could be another type reporter = null; } } } /** * Scan recursively. * * @param dir top directory to scan * @param bDeepScan whether we want to perform a deep scan (read tags again) */ private void scanRecursively(final Directory dir, final boolean bDeepScan) { dir.scan(bDeepScan, reporter); if (reporter != null) { reporter.updateState(dir); } final File[] files = dir.getFio().listFiles(UtilSystem.getDirFilter()); if (files != null) { for (final File element : files) { // Leave ASAP if exit request if (ExitService.isExiting()) { return; } final Directory subDir = DirectoryManager.getInstance().registerDirectory( element.getName(), dir, this); scanRecursively(subDir, bDeepScan); } } } /** * Sets the url. * * @param url The sUrl to set. */ public void setUrl(final String url) { sUrl = url; setProperty(Const.XML_URL, url); fio = new File(url); /** Reset files */ for (final org.jajuk.base.File file : FileManager.getInstance().getFiles()) { file.reset(); } /** Reset playlists */ for (final Playlist plf : PlaylistManager.getInstance().getPlaylists()) { plf.reset(); } /** Reset directories */ for (final Directory dir : DirectoryManager.getInstance().getDirectories()) { dir.reset(); } // Reset the root dir rootDir = null; } /** * Synchronizing asynchronously. * * @param bAsynchronous : * set asynchronous or synchronous mode */ public void synchronize(final boolean bAsynchronous) { // Check a source device is defined if (StringUtils.isBlank((String) getValue(Const.XML_DEVICE_SYNCHRO_SOURCE))) { Messages.showErrorMessage(171); return; } final Device device = this; if (!device.isMounted()) { try { device.mount(true); } catch (final Exception e) { Log.error(11, getName(), e); // mount failed Messages.showErrorMessage(11, getName()); return; } } if (bAsynchronous) { final Thread t = new Thread("Device Synchronize Thread") { @Override public void run() { synchronizeCommand(); } }; t.setPriority(Thread.MIN_PRIORITY); t.start(); } else { synchronizeCommand(); } } /** * Synchronize action itself. */ void synchronizeCommand() { try { bAlreadySynchronizing = true; long lTime = System.currentTimeMillis(); int iNbCreatedFilesDest = 0; int iNbCreatedFilesSrc = 0; lVolume = 0; final boolean bidi = getValue(Const.XML_DEVICE_SYNCHRO_MODE).equals( Const.DEVICE_SYNCHRO_MODE_BI); // check this device is synchronized final String sIdSrc = (String) getValue(Const.XML_DEVICE_SYNCHRO_SOURCE); if (StringUtils.isBlank(sIdSrc) || sIdSrc.equals(getID())) { // cannot synchro with itself return; } final Device dSrc = DeviceManager.getInstance().getDeviceByID(sIdSrc); // perform a fast refresh refreshCommand(false, true, null); // if bidi sync, refresh the other device as well (new file can // have been copied to it) if (bidi) { dSrc.refreshCommand(false, true, null); } // start message InformationJPanel.getInstance().setMessage( new StringBuilder(Messages.getString("Device.31")).append(dSrc.getName()).append(',') .append(getName()).append("]").toString(), InformationJPanel.MessageType.INFORMATIVE); // in both cases (bi or uni-directional), make an unidirectional // sync from source device to this one iNbCreatedFilesDest = synchronizeUnidirectonal(dSrc, this); // now the other one if bidi if (bidi) { iNbCreatedFilesDest += synchronizeUnidirectonal(this, dSrc); } // end message lTime = System.currentTimeMillis() - lTime; final String sOut = new StringBuilder(Messages.getString("Device.33")) .append(((lTime < 1000) ? lTime + " ms" : lTime / 1000 + " s")).append(" - ") .append(iNbCreatedFilesSrc + iNbCreatedFilesDest).append(Messages.getString("Device.35")) .append(lVolume / 1048576).append(Messages.getString("Device.36")).toString(); // perform a fast refresh refreshCommand(false, true, null); // if bidi sync, refresh the other device as well (new file can // have been copied to it) if (bidi) { dSrc.refreshCommand(false, true, null); } InformationJPanel.getInstance().setMessage(sOut, InformationJPanel.MessageType.INFORMATIVE); Log.debug(sOut); } catch (final RuntimeException e) { // runtime errors are thrown throw e; } catch (final Exception e) { // and regular ones logged Log.error(e); } finally { // make sure to unlock synchronizing even if an error occurred bAlreadySynchronizing = false; // Refresh GUI ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_REFRESH)); } } /** * Synchronize a device with another one (unidirectional). * * @param dSrc * @param dest * * @return nb of created files */ private int synchronizeUnidirectonal(final Device dSrc, final Device dest) { final Set<Directory> hsSourceDirs = new HashSet<Directory>(100); // contains paths ( relative to device) of desynchronized dirs final Set<String> hsDesynchroPaths = new HashSet<String>(10); final Set<Directory> hsDestDirs = new HashSet<Directory>(100); int iNbCreatedFiles = 0; List<Directory> dirs = DirectoryManager.getInstance().getDirectories(); for (Directory dir : dirs) { if (dir.getDevice().equals(dSrc)) { // don't take desynchronized dirs into account if (dir.getBooleanValue(Const.XML_DIRECTORY_SYNCHRONIZED)) { hsSourceDirs.add(dir); } else { hsDesynchroPaths.add(dir.getRelativePath()); } } } for (Directory dir : dirs) { if (dir.getDevice().equals(dest)) { if (dir.getBooleanValue(Const.XML_DIRECTORY_SYNCHRONIZED)) { // don't take desynchronized dirs into account hsDestDirs.add(dir); } else { hsDesynchroPaths.add(dir.getRelativePath()); } } } // handle known extensions and image files final FileFilter filter = new JajukFileFilter(false, new JajukFileFilter[] { KnownTypeFilter.getInstance(), ImageFilter.getInstance() }); for (Directory dir : hsSourceDirs) { // give a chance to exit during sync if (ExitService.isExiting()) { return iNbCreatedFiles; } boolean bNeedCreate = true; final String sPath = dir.getRelativePath(); // check the directory on source is not desynchronized. If it // is, leave without checking files if (hsDesynchroPaths.contains(sPath)) { continue; } for (Directory dir2 : hsDestDirs) { if (dir2.getRelativePath().equals(sPath)) { // directory already exists on this device bNeedCreate = false; break; } } // create it if needed final File fileNewDir = new File(new StringBuilder(dest.getUrl()).append(sPath).toString()); if (bNeedCreate && !fileNewDir.mkdirs()) { Log.warn("Could not create directory " + fileNewDir); } // synchronize files final File fileSrc = new File(new StringBuilder(dSrc.getUrl()).append(sPath).toString()); final File[] fSrcFiles = fileSrc.listFiles(filter); if (fSrcFiles != null) { for (final File element : fSrcFiles) { File[] filesArray = fileNewDir.listFiles(filter); if (filesArray == null) { // fileNewDir is not a directory or an error occurred ( // read/write right ? ) continue; } final List<File> files = Arrays.asList(filesArray); // Sort so files are copied in the filesystem order Collections.sort(files); boolean bNeedCopy = true; for (final File element2 : files) { if (element.getName().equalsIgnoreCase(element2.getName())) { bNeedCopy = false; } } if (bNeedCopy) { try { UtilSystem.copyToDir(element, fileNewDir); iNbCreatedFiles++; lVolume += element.length(); InformationJPanel.getInstance().setMessage( new StringBuilder(Messages.getString("Device.41")).append(dSrc.getName()) .append(',').append(dest.getName()).append(Messages.getString("Device.42")) .append(element.getAbsolutePath()).append("]").toString(), InformationJPanel.MessageType.INFORMATIVE); } catch (final JajukException je) { Messages.showErrorMessage(je.getCode(), element.getAbsolutePath()); Messages.showErrorMessage(27); Log.error(je); return iNbCreatedFiles; } catch (final Exception e) { Messages.showErrorMessage(20, element.getAbsolutePath()); Messages.showErrorMessage(27); Log.error(20, "{{" + element.getAbsolutePath() + "}}", e); return iNbCreatedFiles; } } } } } return iNbCreatedFiles; } /** * Test device accessibility. * * @return true if the device is available */ public boolean test() { UtilGUI.waiting(); // waiting cursor boolean bOK = false; boolean bWasMounted = bMounted; // store mounted state of device before // mount test try { if (!bMounted) { mount(true); } } catch (final Exception e) { UtilGUI.stopWaiting(); return false; } if (getLongValue(Const.XML_TYPE) != 5) { // not a remote device final File file = new File(sUrl); if (file.exists() && file.canRead()) { // see if the url exists // and is readable // check if this device was void boolean bVoid = true; for (org.jajuk.base.File f : FileManager.getInstance().getFiles()) { if (f.getDirectory().getDevice().equals(this)) { // at least one field in this device bVoid = false; break; } } if (!bVoid) { // if the device is not supposed to be void, // check if it is the case, if no, the device // must not be unix-mounted if (file.list().length > 0) { bOK = true; } } else { // device is void, OK we assume it is accessible bOK = true; } } } else { bOK = false; // TBI } // unmount the device if it was mounted only for the test if (!bWasMounted) { try { unmount(false, false); } catch (final Exception e1) { Log.error(e1); } } UtilGUI.stopWaiting(); return bOK; } /** * toString method. * * @return the string */ @Override public String toString() { return "Device[ID=" + getID() + " Name=" + getName() + " Type=" + getType().name() + " URL=" + sUrl + "]"; } /** * Unmount the device. */ public void unmount() { unmount(false, true); } /** * Unmount the device with ejection. * * @param bEjection set whether the device must be ejected * @param bUIRefresh set whether the UI should be refreshed */ public void unmount(final boolean bEjection, final boolean bUIRefresh) { // look to see if the device is already mounted if (!bMounted) { Messages.showErrorMessage(125); // already unmounted return; } // ask fifo if it doens't use any track from this device if (!QueueModel.canUnmount(this)) { Messages.showErrorMessage(121); return; } bMounted = false; if (bUIRefresh) { ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_UNMOUNT)); } } }