/*
* 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.util;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.swing.JOptionPane;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.jajuk.base.Album;
import org.jajuk.base.AlbumManager;
import org.jajuk.base.Device;
import org.jajuk.base.DeviceManager;
import org.jajuk.base.SearchResult.SearchResultType;
import org.jajuk.base.Track;
import org.jajuk.base.TrackManager;
import org.jajuk.services.core.SessionService;
import org.jajuk.services.dj.AmbienceManager;
import org.jajuk.services.webradio.WebRadioHelper;
import org.jajuk.ui.thumbnails.ThumbnailManager;
import org.jajuk.util.log.Log;
/**
* Maintain all behavior needed upgrades from releases to releases.
*
* Jajuk version sheme is XX.YY.ZZ (two digits possible for each part of the release)
*/
public final class UpgradeManager implements Const {
/** Last jajuk release known from Internet (parsed from a pad file). */
private static String newVersionName;
/** Is it a minor or major X.Y upgrade */
private static boolean bUpgraded = false;
/** Is it the first session ever ?. */
private static boolean firstSession = false;
/** Is it an old migration (more than 1 major release) ?. */
private static boolean majorMigration = false;
/** List of versions that doesn't require perspective reset at upgrade. */
private static String[] versionsNoNeedPerspectiveReset = new String[] { "1.9" };
/**
* private constructor to avoid instantiating utility class.
*/
private UpgradeManager() {
}
/**
* Return Jajuk number version = integer format of the padded release
*
* Jajuk version scheme is XX.YY.ZZ[RCn] (two digits possible for each part of the release)
*
* @param pStringRelease
*
* @return Jajuk number version = integer format of the padded release
*/
static int getNumberRelease(String pStringRelease) {
if (pStringRelease == null) {
// no string provided: use 1.0.0
return 10000;
}
String stringRelease = pStringRelease;
// We drop any RCx part of the release
if (pStringRelease.contains("RC")) {
stringRelease = pStringRelease.split("RC.*")[0];
}
// We drop any "dev" part of the release
if (pStringRelease.contains("dev")) {
stringRelease = pStringRelease.split("dev.*")[0];
}
// Add a trailing .0 if it is a main release like 1.X -> 1.X.0
int countDot = StringUtils.countMatches(stringRelease, ".");
if (countDot == 1) {
stringRelease = stringRelease + ".0";
}
// Analyze each part of the release, throw a runtime exception if
// the format is wrong at this point
StringTokenizer st = new StringTokenizer(stringRelease, ".");
int main = 10000 * Integer.parseInt(st.nextToken());
int minor = 100 * Integer.parseInt(st.nextToken());
int fix = Integer.parseInt(st.nextToken());
return main + minor + fix;
}
/**
* Detect current release and if an upgrade occurred since last startup.
*/
public static void detectRelease() {
try {
// In dev, don't try to upgrade
if ("VERSION_REPLACED_BY_ANT".equals(Const.JAJUK_VERSION)) {
bUpgraded = false;
majorMigration = false;
return;
}
// Upgrade detection. Depends on: Configuration manager load
final String sStoredRelease = Conf.getString(Const.CONF_RELEASE);
// check if it is a new major 'x.y' release: 1.2 != 1.3 for instance
if (!firstSession
// if first session, not taken as an upgrade
&& (sStoredRelease == null || // null for jajuk releases < 1.2
!sStoredRelease.equals(Const.JAJUK_VERSION))) {
bUpgraded = true;
if (!SessionService.isTestMode()) {
if (isMajorMigration(Const.JAJUK_VERSION, sStoredRelease)) {
majorMigration = true;
}
}
}
} catch (Exception e) {
Log.error(e);
}
if (SessionService.isTestMode()) {
// In test mode, we are always in upgraded mode
bUpgraded = true;
}
// Now set current release in the conf
Conf.setProperty(Const.CONF_RELEASE, Const.JAJUK_VERSION);
}
/**
* Checks if is first session.
*
* @return true, if is first session
*/
public static boolean isFirstSession() {
return firstSession;
}
/**
* Sets the first session.
*
*/
public static void setFirstSession() {
firstSession = true;
}
/**
* Actions to migrate an existing installation.
*
* Step 1 : before collection loading
*/
public static void upgradeStep1() {
// We ignore errors during upgrade
try {
if (isUpgradeDetected()) {
// For jajuk < 0.2
upgradeOldCollectionBackupFile();
// For Jajuk < 1.2
upgradeDefaultAmbience();
// For Jajuk < 1.3
upgradeTrackPattern();
upgradeSerFiles();
upgradeNocover();
upgradeWrongHotketOption();
// For Jajuk < 1.4
upgradePerspectivesRename();
// For Jajuk < 1.6
upgradePerspectiveButtonsSize();
upgradeDJClassChanges();
// For Jajuk < 1.7
upgradeElapsedTimeFormat();
// for Jajuk < 1.9
upgradeAlarmConfFile();
upgradeStartupConf();
// for Jajuk < 1.10
upgradeWebRadioFile();
// for jajuk < 1.10.5
upgradeCollectionExitFile();
}
} catch (Exception e) {
Log.error(e);
}
}
/**
* For Jajuk < 0.2 : remove backup file : collection~.xml
*
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void upgradeOldCollectionBackupFile() throws IOException {
File file = SessionService.getConfFileByPath(Const.FILE_COLLECTION + "~");
if (file.exists()) {
UtilSystem.deleteFile(file);
}
}
/**
* For Jajuk <1.2, set default ambiences
*/
private static void upgradeDefaultAmbience() {
String sRelease = Conf.getString(Const.CONF_RELEASE);
if (sRelease == null || sRelease.matches("0..*") || sRelease.matches("1.0..*")
|| sRelease.matches("1.1.*")) {
AmbienceManager.getInstance().createDefaultAmbiences();
}
}
/**
* For Jajuk < 1.3 : changed track pattern from %track to %title
*/
private static void upgradeTrackPattern() {
String sPattern = Conf.getString(Const.CONF_PATTERN_REFACTOR);
if (sPattern.contains("track")) {
Conf.setProperty(Const.CONF_PATTERN_REFACTOR, sPattern.replaceAll("track", "title"));
}
}
/**
* For Jajuk < 1.3: no more use of .ser files
*/
private static void upgradeSerFiles() {
File file = SessionService.getConfFileByPath("");
File[] files = file.listFiles();
for (File element : files) {
// delete all .ser files
if (UtilSystem.getExtension(element).equals("ser")) {
try {
UtilSystem.deleteFile(element);
} catch (IOException e) {
Log.error(e);
}
}
}
}
/**
* For Jajuk < 1.9.3: 'cover' tag can't contain "none" string
*/
private static void upgradeNoneCover() {
for (Album album : AlbumManager.getInstance().getAlbums()) {
if (COVER_NONE.equals(album.getStringValue(XML_ALBUM_SELECTED_COVER))) {
album.setProperty(XML_ALBUM_SELECTED_COVER, "");
}
}
}
/**
* For Jajuk < 1.9: bootstrap file is now in XML format
* <br>
* If it exists and contains data in 1.7 or 1.8 format, it convert it to new XML
* format (to handle backslashes properly, old format just drop them)
* <br>
* This method doesn't yet validate provided workspace paths but only the bootstrap file
* structure itself.
*/
public static void upgradeBootstrapFile() {
try {
String KEY_TEST = "test";
String KEY_FINAL = "final";
File bootstrapOld = new File(SessionService.getBootstrapPath(Const.FILE_BOOTSTRAP_OLD));
File bootstrapOldOldHome = new File(System.getProperty("user.home") + "/"
+ Const.FILE_BOOTSTRAP_OLD);
File bootstrapNew = new File(SessionService.getBootstrapPath());
// Fix for #1473 : move the bootstrap file if required (See #1473)
if (UtilSystem.isUnderWindows() && !bootstrapOld.equals(bootstrapOldOldHome)
&& !bootstrapOld.exists() && bootstrapOldOldHome.exists()) {
try {
FileUtils.copyFileToDirectory(bootstrapOldOldHome, new File(UtilSystem.getUserHome()));
UtilSystem.deleteFile(bootstrapOldOldHome);
} catch (IOException ex) {
ex.printStackTrace();
}
}
if (bootstrapOld.exists() && !bootstrapNew.exists()) {
Properties prop = null;
// Try to load a bootstrap file using plain text old format
prop = new Properties();
FileInputStream fis = new FileInputStream(
SessionService.getBootstrapPath(Const.FILE_BOOTSTRAP_OLD));
prop.load(fis);
fis.close();
// If it exists and contains pre-1.7 bootstrap format (a single line with a raw path),
// convert it to 1.7 format first
if (prop.size() == 1) {
// We get something like <... path ...> = <nothing>
String path = (String) prop.keys().nextElement();
// we use this path for both test and final workspace
prop.clear();
prop.put(KEY_TEST, path);
prop.put(KEY_FINAL, path);
}
// Make sure to populate both test and final release
if (!prop.containsKey(KEY_TEST)) {
prop.put(KEY_TEST, UtilSystem.getUserHome());
}
if (!prop.containsKey(KEY_FINAL)) {
prop.put(KEY_FINAL, UtilSystem.getUserHome());
}
// Write down the new bootstrap file
SessionService.commitBootstrapFile(prop);
// Delete old bootstrap file
bootstrapOld.delete();
}
} catch (Exception e) {
// Do not throw any exception from here. display raw stack trace, Logs facilities
// are not yet available.
e.printStackTrace();
}
}
/**
* For Jajuk < 1.3: force nocover thumb replacement
*/
private static void upgradeNocover() {
upgradeNoCoverDelete("50x50");
upgradeNoCoverDelete("100x100");
upgradeNoCoverDelete("150x150");
upgradeNoCoverDelete("200x200");
}
/**
* For Jajuk < 1.3: delete thumb for given size
*
* @param size
*/
private static void upgradeNoCoverDelete(String size) {
File fThumbs = SessionService.getConfFileByPath(Const.FILE_THUMBS + "/" + size + "/"
+ Const.FILE_THUMB_NO_COVER);
if (fThumbs.exists()) {
try {
UtilSystem.deleteFile(fThumbs);
} catch (IOException e) {
Log.error(e);
}
}
}
/**
* jajuk 1.3: wrong option name: "false" instead of
* "jajuk.options.use_hotkeys"
*/
private static void upgradeWrongHotketOption() {
String sUseHotkeys = Conf.getString("false");
if (sUseHotkeys != null) {
if (sUseHotkeys.equalsIgnoreCase(Const.FALSE) || sUseHotkeys.equalsIgnoreCase(Const.TRUE)) {
Conf.setProperty(Const.CONF_OPTIONS_HOTKEYS, sUseHotkeys);
Conf.removeProperty("false");
} else {
Conf.setProperty(Const.CONF_OPTIONS_HOTKEYS, Const.FALSE);
}
}
}
/**
* For jajuk < 1.9: Alarm configuration, file / webradio to be launched
*/
private static void upgradeAlarmConfFile() {
String conf = Conf.getString(Const.CONF_ALARM_FILE);
if (conf.indexOf('/') == -1) {
conf = SearchResultType.FILE.name() + '/' + conf;
Conf.setProperty(Const.CONF_ALARM_FILE, conf);
}
}
/**
* For jajuk < 1.9: Startup configuration, file / webradio to be launched
*/
private static void upgradeStartupConf() {
String conf = Conf.getString(Const.CONF_STARTUP_ITEM);
// conf = "" if none track has never been launched or if
// jajuk was closed in stopped state
if (!conf.equals("") && conf.indexOf('/') == -1) {
conf = SearchResultType.FILE.name() + '/' + conf;
Conf.setProperty(Const.CONF_STARTUP_ITEM, conf);
}
}
/**
* For jajuk <1.4 (or early 1.4), some perspectives have been renamed
*/
private static void upgradePerspectivesRename() {
upgradePerspectivesRenameDelete("LogicalPerspective.xml");
upgradePerspectivesRenameDelete("PhysicalPerspective.xml");
upgradePerspectivesRenameDelete("CatalogPerspective.xml");
upgradePerspectivesRenameDelete("PlayerPerspective.xml");
upgradePerspectivesRenameDelete("HelpPerspective.xml");
}
/**
* For jajuk <1.4 (or early 1.4), delete renamed perspectives names
*
* @param name : perspective filename
*/
private static void upgradePerspectivesRenameDelete(String name) {
File fPerspective = SessionService.getConfFileByPath(name);
if (fPerspective.exists()) {
try {
UtilSystem.deleteFile(fPerspective);
} catch (IOException e) {
Log.error(e);
}
}
}
/**
* Jajuk < 1.6. Perspective buttons size changed.
*/
private static void upgradePerspectiveButtonsSize() {
if (Conf.getInt(Const.CONF_PERSPECTIVE_ICONS_SIZE) > 45) {
Conf.setProperty(Const.CONF_PERSPECTIVE_ICONS_SIZE, "45");
}
// For Jajuk 1.5 and jajuk 1.6 columns conf id changed
if (Conf.getString(Const.CONF_PLAYLIST_REPOSITORY_COLUMNS).matches(".*0.*")) {
Conf.setDefaultProperty(Const.CONF_PLAYLIST_REPOSITORY_COLUMNS);
}
if (Conf.getString(Const.CONF_QUEUE_COLUMNS).matches(".*0.*")) {
Conf.setDefaultProperty(Const.CONF_QUEUE_COLUMNS);
}
if (Conf.getString(Const.CONF_PLAYLIST_EDITOR_COLUMNS).matches(".*0.*")) {
Conf.setDefaultProperty(Const.CONF_PLAYLIST_EDITOR_COLUMNS);
}
}
/**
* For Jajuk < 1.6 (DJ classes changed)
*/
private static void upgradeDJClassChanges() {
File[] files = SessionService.getConfFileByPath(Const.FILE_DJ_DIR).listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
if (file.isFile() && file.getPath().endsWith('.' + Const.XML_DJ_EXTENSION)) {
return true;
}
return false;
}
});
for (File dj : files) {
if (UtilSystem.replaceInFile(dj, "org.jajuk.dj.ProportionDigitalDJ",
Const.XML_DJ_PROPORTION_CLASS, "UTF-8")) {
Log.info("Migrated DJ file: {{" + dj.getName() + "}}");
}
if (UtilSystem.replaceInFile(dj, "org.jajuk.dj.TransitionDigitalDJ",
Const.XML_DJ_TRANSITION_CLASS, "UTF-8")) {
Log.info("Migrated DJ file: {{" + dj.getName() + "}}");
}
if (UtilSystem.replaceInFile(dj, "org.jajuk.dj.AmbienceDigitalDJ",
Const.XML_DJ_AMBIENCE_CLASS, "UTF-8")) {
Log.info("Migrated DJ file: {{" + dj.getName() + "}}");
}
}
}
/**
* For Jajuk < 1.7, elapsed time format variable name changed
*/
private static void upgradeElapsedTimeFormat() {
if (Conf.containsProperty("format")) {
Conf.setProperty(Const.CONF_FORMAT_TIME_ELAPSED, Conf.getString("format"));
}
}
/**
* For jajuk < 1.7, Update rating system
*/
private static void upgradeCollectionRating() {
String sRelease = Conf.getString(Const.CONF_RELEASE);
if (sRelease == null || isOlder(sRelease, "1.7")) {
Log.info("Migrating collection rating");
// We keep current ratings and we recompute them on a 0 to 100 scale,
// then we suggest user to reset the rates
// Start by finding max (old) rating
long maxRating = 0;
ReadOnlyIterator<Track> tracks = TrackManager.getInstance().getTracksIterator();
while (tracks.hasNext()) {
Track track = tracks.next();
if (track.getRate() > maxRating) {
maxRating = track.getRate();
}
}
// Then apply the new rating
for (Track track : TrackManager.getInstance().getTracks()) {
long newRate = (long) (100f * track.getRate() / maxRating);
TrackManager.getInstance().changeTrackRate(track, newRate);
}
Log.info("Migrating rating done");
Messages.showInfoMessage(Messages.getString("Note.1"));
}
}
/**
* For jajuk < 1.9, remove album artist property for albums
*/
private static void upgradeNoMoreAlbumArtistsForAlbums() {
if (AlbumManager.getInstance().getMetaInformation(Const.XML_ALBUM_ARTIST) != null) {
AlbumManager.getInstance().removeProperty(Const.XML_ALBUM_ARTIST);
}
}
/**
* For jajuk < 1.10, upgrade webradio files
*/
private static void upgradeWebRadioFile() {
try {
File oldFile = SessionService.getConfFileByPath("webradios.xml");
if (oldFile.exists()) {
Log.info("Migrating old webradio file : " + oldFile.getAbsolutePath());
File newCustomFile = SessionService.getConfFileByPath(Const.FILE_WEB_RADIOS_CUSTOM);
UtilSystem.move(oldFile, newCustomFile);
//Load the old file (contains presets + real customs files)
WebRadioHelper.loadCustomRadios();
// Download and load the real preset files to override customs and set them 'PRESET' origin
// Download repository
File fPresets = SessionService.getConfFileByPath(Const.FILE_WEB_RADIOS_PRESET);
DownloadManager.download(new URL(Const.URL_WEBRADIO_PRESETS), fPresets);
WebRadioHelper.loadPresetsRadios(fPresets);
}
} catch (Exception e) {
Log.debug("Can't upgrade Webradio file", e);
}
}
/**
* For jajuk < 1.10.5, move collection_exit.xml to collection.xml
*/
private static void upgradeCollectionExitFile() {
try {
File oldFile = SessionService.getConfFileByPath("collection_exit.xml");
if (oldFile.exists()) {
Log.info("Migrating old collection_exit file to collection.xml");
File newCollectionFile = SessionService.getConfFileByPath(Const.FILE_COLLECTION);
UtilSystem.move(oldFile, newCollectionFile);
}
} catch (Exception e) {
Log.debug("Can't migrate collection_exit.xml file", e);
}
}
/**
* For any jajuk version, after major upgrade, force thumbs cleanup.
*/
private static void upgradeThumbRebuild() {
// Rebuild thumbs when upgrading
new Thread() {
@Override
public void run() {
// Clean thumbs
ThumbnailManager.cleanThumbs(Const.THUMBNAIL_SIZE_50X50);
ThumbnailManager.cleanThumbs(Const.THUMBNAIL_SIZE_100X100);
ThumbnailManager.cleanThumbs(Const.THUMBNAIL_SIZE_150X150);
ThumbnailManager.cleanThumbs(Const.THUMBNAIL_SIZE_200X200);
ThumbnailManager.cleanThumbs(Const.THUMBNAIL_SIZE_250X250);
ThumbnailManager.cleanThumbs(Const.THUMBNAIL_SIZE_300X300);
}
}.start();
}
/**
* Actions to migrate an existing installation.
*
* Step 2 after collection load
*/
public static void upgradeStep2() {
try {
if (isUpgradeDetected()) {
// For Jajuk < 1.7
upgradeCollectionRating();
// For Jajuk < 1.9
upgradeNoMoreAlbumArtistsForAlbums();
// For Jajuk < 1.9.3
upgradeNoneCover();
}
// Major releases upgrade specific operations
if (isMajorMigration()) {
upgradeThumbRebuild();
}
} catch (Throwable e) {
Log.error(e);
}
}
/**
* Actions to migrate an existing installation.
*
* Step 3 after full jajuk startup
*/
public static void upgradeStep3() {
try {
// Major releases upgrade specific operations
if (isMajorMigration()) {
deepScanRequest();
}
} catch (Throwable e) {
Log.error(e);
}
}
/**
* Checks if is upgrade detected.
*
* @return true if it is the first session after a minor or major upgrade
* session
*/
public static boolean isUpgradeDetected() {
return bUpgraded;
}
/**
* Check for a new Jajuk release.
*
* @return true if a new release has been found
*/
public static void checkForUpdate() {
// If test mode, don't try to update
if (SessionService.isTestMode()) {
return;
}
// Try to download current jajuk PAD file
String sPadRelease = null;
try {
String pad = DownloadManager.downloadText(new URL(Const.CHECK_FOR_UPDATE_URL));
int beginIndex = pad.indexOf("<Program_Version>");
int endIndex = pad.indexOf("</Program_Version>");
sPadRelease = pad.substring(beginIndex + 17, endIndex);
if (!Const.JAJUK_VERSION.equals(sPadRelease)
// Don't use this in test
&& !("VERSION_REPLACED_BY_ANT".equals(Const.JAJUK_VERSION))
// We display the upgrade icon only if PAD release is newer than current release
&& isNewer(sPadRelease, Const.JAJUK_VERSION)) {
newVersionName = sPadRelease;
return;
}
} catch (Exception e) {
Log.debug("Cannot check for updates", e);
}
return;
}
/**
* Gets the new version name.
*
* @return new version name if nay
* <p>
* Example: "1.6", "1.7.8"
*/
public static String getNewVersionName() {
return newVersionName;
}
/**
* Is it an old migration (more than 1 major release) ? *.
*
* @return true, if checks if is major migration
*/
public static boolean isMajorMigration() {
return majorMigration;
}
/**
* Return whether two releases switch is a major upgrade or not.
*
* @param currentRelease
* @param comparedRelease
*
* @return whether two releases switch is a major upgrade or not
*/
protected static boolean isMajorMigration(String codeRelease, String comparedRelease) {
int iCurrentRelease = getNumberRelease(codeRelease);
int iComparedRelease = getNumberRelease(comparedRelease);
return iComparedRelease / 100 != iCurrentRelease / 100;
}
/**
* Return whether first release is newer than second.
*
* @param currentRelease
* @param comparedRelease
*
* @return whether first release is newer than second
*/
protected static boolean isNewer(String comparedRelease, String currentRelease) {
int iCurrentRelease = getNumberRelease(currentRelease);
int iComparedRelease = getNumberRelease(comparedRelease);
return iComparedRelease > iCurrentRelease;
}
/**
* Return whether first release is older than second.
*
* @param currentRelease
* @param comparedRelease
*
* @return whether first release is newer than second
*/
protected static boolean isOlder(String comparedRelease, String currentRelease) {
// Manage dev case
if ("VERSION_REPLACED_BY_ANT".equals(comparedRelease)
|| "VERSION_REPLACED_BY_ANT".equals(currentRelease)) {
return false;
}
int iCurrentRelease = getNumberRelease(currentRelease);
int iComparedRelease = getNumberRelease(comparedRelease);
return iComparedRelease < iCurrentRelease;
}
/**
* Require user to perform a deep scan.
*/
private static void deepScanRequest() {
int reply = Messages.getChoice(Messages.getString("Warning.7"),
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
if (reply == JOptionPane.CANCEL_OPTION || reply == JOptionPane.NO_OPTION) {
return;
}
if (reply == JOptionPane.YES_OPTION) {
final Thread t = new Thread("Device Refresh Thread after upgrade") {
@Override
public void run() {
List<Device> devices = DeviceManager.getInstance().getDevices();
for (Device device : devices) {
if (device.isReady()) {
device.manualRefresh(false, false, true, null);
}
}
}
};
t.setPriority(Thread.MIN_PRIORITY);
t.start();
}
}
/**
* Return whether this version need a perspective reset at upgrade.
* We reset perspectives only at major upgrade and if it comes with new views.
*
* @return whether this version need a perspective reset at upgrade
*/
public static boolean doNeedPerspectiveResetAtUpgrade() {
if (!isMajorMigration()) {
return false;
}
for (String version : versionsNoNeedPerspectiveReset) {
if (Const.JAJUK_VERSION.matches(version + ".*")) {
return false;
}
}
return true;
}
}