/*
* IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
*
* http://izpack.org/
* http://izpack.codehaus.org/
*
* Copyright 2007 Dennis Reil
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.izforge.izpack.installer;
import com.izforge.izpack.LocaleDatabase;
import com.izforge.izpack.Pack;
import com.izforge.izpack.PackFile;
import com.izforge.izpack.UpdateCheck;
import com.izforge.izpack.event.InstallerListener;
import com.izforge.izpack.rules.RulesEngine;
import com.izforge.izpack.util.AbstractUIProgressHandler;
import com.izforge.izpack.util.Debug;
import com.izforge.izpack.util.IoHelper;
import com.izforge.izpack.util.VariableSubstitutor;
import org.apache.regexp.RE;
import org.apache.regexp.RECompiler;
import org.apache.regexp.RESyntaxException;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* Abstract base class for all unpacker implementations.
*
* @author Dennis Reil, <izpack@reil-online.de>
*/
public abstract class UnpackerBase implements IUnpacker
{
/**
* The installdata.
*/
protected AutomatedInstallData idata;
/**
* The installer listener.
*/
protected AbstractUIProgressHandler handler;
/**
* The uninstallation data.
*/
protected UninstallData udata;
/**
* The variables substitutor.
*/
protected VariableSubstitutor vs;
/**
* The absolute path of the installation. (NOT the canonical!)
*/
protected File absolute_installpath;
/**
* The absolute path of the source installation jar.
*/
private File absolutInstallSource;
/**
* The packs locale database.
*/
protected LocaleDatabase langpack = null;
/**
* The result of the operation.
*/
protected boolean result = true;
/**
* The instances of the unpacker objects.
*/
protected static HashMap<Object, String> instances = new HashMap<Object, String>();
/**
* Interrupt flag if global interrupt is desired.
*/
protected static boolean interruptDesired = false;
/**
* Do not perform a interrupt call.
*/
protected static boolean discardInterrupt = false;
/**
* The name of the XML file that specifies the panel langpack
*/
protected static final String LANG_FILE_NAME = "packsLang.xml";
public static final String ALIVE = "alive";
public static final String INTERRUPT = "doInterrupt";
public static final String INTERRUPTED = "interruppted";
protected RulesEngine rules;
/**
* The constructor.
*
* @param idata The installation data.
* @param handler The installation progress handler.
*/
public UnpackerBase(AutomatedInstallData idata, AbstractUIProgressHandler handler)
{
try
{
String resource = LANG_FILE_NAME + "_" + idata.localeISO3;
this.langpack = new LocaleDatabase(ResourceManager.getInstance().getInputStream(resource));
}
catch (Throwable exception)
{
}
this.idata = idata;
this.handler = handler;
// Initialize the variable substitutor
vs = new VariableSubstitutor(idata.getVariables());
}
public void setRules(RulesEngine rules)
{
this.rules = rules;
}
/**
* Returns a copy of the active unpacker instances.
*
* @return a copy of active unpacker instances
*/
public static HashMap getRunningInstances()
{
synchronized (instances)
{ // Return a shallow copy to prevent a
// ConcurrentModificationException.
return (HashMap) (instances.clone());
}
}
/**
* Adds this to the map of all existent instances of Unpacker.
*/
protected void addToInstances()
{
synchronized (instances)
{
instances.put(this, ALIVE);
}
}
/**
* Removes this from the map of all existent instances of Unpacker.
*/
protected void removeFromInstances()
{
synchronized (instances)
{
instances.remove(this);
}
}
/**
* Initiate interrupt of all alive Unpacker. This method does not interrupt the Unpacker objects
* else it sets only the interrupt flag for the Unpacker objects. The dispatching of interrupt
* will be performed by the Unpacker objects self.
*/
private static void setInterruptAll()
{
synchronized (instances)
{
Iterator iter = instances.keySet().iterator();
while (iter.hasNext())
{
Object key = iter.next();
if (instances.get(key).equals(ALIVE))
{
instances.put(key, INTERRUPT);
}
}
// Set global flag to allow detection of it in other classes.
// Do not set it to thread because an exec will then be stoped.
setInterruptDesired(true);
}
}
/**
* Initiate interrupt of all alive Unpacker and waits until all Unpacker are interrupted or the
* wait time has arrived. If the doNotInterrupt flag in InstallerListener is set to true, the
* interrupt will be discarded.
*
* @param waitTime wait time in millisecounds
* @return true if the interrupt will be performed, false if the interrupt will be discarded
*/
public static boolean interruptAll(long waitTime)
{
long t0 = System.currentTimeMillis();
if (isDiscardInterrupt())
{
return (false);
}
setInterruptAll();
while (!isInterruptReady())
{
if (System.currentTimeMillis() - t0 > waitTime)
{
return (true);
}
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
}
}
return (true);
}
private static boolean isInterruptReady()
{
synchronized (instances)
{
Iterator iter = instances.keySet().iterator();
while (iter.hasNext())
{
Object key = iter.next();
if (!instances.get(key).equals(INTERRUPTED))
{
return (false);
}
}
return (true);
}
}
/**
* Sets the interrupt flag for this Unpacker to INTERRUPTED if the previos state was INTERRUPT
* or INTERRUPTED and returns whether interrupt was initiate or not.
*
* @return whether interrupt was initiate or not
*/
protected boolean performInterrupted()
{
synchronized (instances)
{
Object doIt = instances.get(this);
if (doIt != null && (doIt.equals(INTERRUPT) || doIt.equals(INTERRUPTED)))
{
instances.put(this, INTERRUPTED);
this.result = false;
return (true);
}
return (false);
}
}
/**
* Returns whether interrupt was initiate or not for this Unpacker.
*
* @return whether interrupt was initiate or not
*/
private boolean shouldInterrupt()
{
synchronized (instances)
{
Object doIt = instances.get(this);
if (doIt != null && (doIt.equals(INTERRUPT) || doIt.equals(INTERRUPTED)))
{
return (true);
}
return (false);
}
}
/**
* Return the state of the operation.
*
* @return true if the operation was successful, false otherwise.
*/
public boolean getResult()
{
return this.result;
}
/**
* @param filename
* @param patterns
* @return true if the file matched one pattern, false if it did not
*/
private boolean fileMatchesOnePattern(String filename, ArrayList<RE> patterns)
{
// first check whether any include matches
for (RE pattern : patterns)
{
if (pattern.match(filename))
{
return true;
}
}
return false;
}
/**
* @param list A list of file name patterns (in ant fileset syntax)
* @param recompiler The regular expression compiler (used to speed up RE compiling).
* @return List of org.apache.regexp.RE
*/
private List<RE> preparePatterns(ArrayList<String> list, RECompiler recompiler)
{
ArrayList<RE> result = new ArrayList<RE>();
for (String element : list)
{
if ((element != null) && (element.length() > 0))
{
// substitute variables in the pattern
element = this.vs.substitute(element, "plain");
// check whether the pattern is absolute or relative
File f = new File(element);
// if it is relative, make it absolute and prepend the
// installation path
// (this is a bit dangerous...)
if (!f.isAbsolute())
{
element = new File(this.absolute_installpath, element).toString();
}
// now parse the element and construct a regular expression from
// it
// (we have to parse it one character after the next because
// every
// character should only be processed once - it's not possible
// to get this
// correct using regular expression replacing)
StringBuffer element_re = new StringBuffer();
int lookahead = -1;
int pos = 0;
while (pos < element.length())
{
char c;
if (lookahead != -1)
{
c = (char) lookahead;
lookahead = -1;
}
else
{
c = element.charAt(pos++);
}
switch (c)
{
case '/':
{
element_re.append(File.separator);
break;
}
// escape backslash and dot
case '\\':
case '.':
{
element_re.append("\\");
element_re.append(c);
break;
}
case '*':
{
if (pos == element.length())
{
element_re.append("[^").append(File.separator).append("]*");
break;
}
lookahead = element.charAt(pos++);
// check for "**"
if (lookahead == '*')
{
element_re.append(".*");
// consume second star
lookahead = -1;
}
else
{
element_re.append("[^").append(File.separator).append("]*");
// lookahead stays there
}
break;
}
default:
{
element_re.append(c);
break;
}
} // switch
}
// make sure that the whole expression is matched
element_re.append('$');
// replace \ by \\ and create a RE from the result
try
{
result.add(new RE(recompiler.compile(element_re.toString())));
}
catch (RESyntaxException e)
{
this.handler.emitNotification("internal error: pattern \"" + element
+ "\" produced invalid RE \"" + f.getPath() + "\"");
}
}
}
return result;
}
// CUSTOM ACTION STUFF -------------- start -----------------
/**
* Informs all listeners which would be informed at the given action type.
*
* @param customActions array of lists with the custom action objects
* @param action identifier for which callback should be called
* @param firstParam first parameter for the call
* @param secondParam second parameter for the call
* @param thirdParam third parameter for the call
*/
protected void informListeners(List[] customActions, int action, Object firstParam,
Object secondParam, Object thirdParam) throws Exception
{
List listener = null;
// select the right action list.
switch (action)
{
case InstallerListener.BEFORE_FILE:
case InstallerListener.AFTER_FILE:
case InstallerListener.BEFORE_DIR:
case InstallerListener.AFTER_DIR:
listener = customActions[customActions.length - 1];
break;
default:
listener = customActions[0];
break;
}
if (listener == null)
{
return;
}
// Iterate the action list.
Iterator iter = listener.iterator();
while (iter.hasNext())
{
if (shouldInterrupt())
{
return;
}
InstallerListener il = (InstallerListener) iter.next();
switch (action)
{
case InstallerListener.BEFORE_FILE:
il.beforeFile((File) firstParam, (PackFile) secondParam);
break;
case InstallerListener.AFTER_FILE:
il.afterFile((File) firstParam, (PackFile) secondParam);
break;
case InstallerListener.BEFORE_DIR:
il.beforeDir((File) firstParam, (PackFile) secondParam);
break;
case InstallerListener.AFTER_DIR:
il.afterDir((File) firstParam, (PackFile) secondParam);
break;
case InstallerListener.BEFORE_PACK:
il.beforePack((Pack) firstParam, (Integer) secondParam,
(AbstractUIProgressHandler) thirdParam);
break;
case InstallerListener.AFTER_PACK:
il.afterPack((Pack) firstParam, (Integer) secondParam,
(AbstractUIProgressHandler) thirdParam);
break;
case InstallerListener.BEFORE_PACKS:
il.beforePacks((AutomatedInstallData) firstParam, (Integer) secondParam,
(AbstractUIProgressHandler) thirdParam);
break;
case InstallerListener.AFTER_PACKS:
il.afterPacks((AutomatedInstallData) firstParam,
(AbstractUIProgressHandler) secondParam);
break;
}
}
}
/**
* Returns the defined custom actions split into types including a constructed type for the file
* related installer listeners.
*
* @return array of lists of custom action data like listeners
*/
protected List[] getCustomActions()
{
String[] listenerNames = AutomatedInstallData.CUSTOM_ACTION_TYPES;
List[] retval = new List[listenerNames.length + 1];
int i;
for (i = 0; i < listenerNames.length; ++i)
{
retval[i] = idata.customData.get(listenerNames[i]);
if (retval[i] == null)
// Make a dummy list, then iterator is ever callable.
{
retval[i] = new ArrayList();
}
}
if (retval[AutomatedInstallData.INSTALLER_LISTENER_INDEX].size() > 0)
{ // Installer listeners exist
// Create file related installer listener list in the last
// element of custom action array.
i = retval.length - 1; // Should be so, but safe is safe ...
retval[i] = new ArrayList();
Iterator iter = retval[AutomatedInstallData.INSTALLER_LISTENER_INDEX]
.iterator();
while (iter.hasNext())
{
// If we get a class cast exception many is wrong and
// we must fix it.
InstallerListener li = (InstallerListener) iter.next();
if (li.isFileListener())
{
retval[i].add(li);
}
}
}
return (retval);
}
// This method is only used if a file related custom action exist.
/**
* Creates the given directory recursive and calls the method "afterDir" of each listener with
* the current file object and the pack file object. On error an exception is raised.
*
* @param dest the directory which should be created
* @param pf current pack file object
* @param customActions all defined custom actions
* @return false on error, true else
* @throws Exception
*/
protected boolean mkDirsWithEnhancement(File dest, PackFile pf, List[] customActions)
throws Exception
{
String path = "unknown";
if (dest != null)
{
path = dest.getAbsolutePath();
}
if (dest != null && !dest.exists() && dest.getParentFile() != null)
{
if (dest.getParentFile().exists())
{
informListeners(customActions, InstallerListener.BEFORE_DIR, dest, pf, null);
}
if (!dest.mkdir())
{
mkDirsWithEnhancement(dest.getParentFile(), pf, customActions);
if (!dest.mkdir())
{
dest = null;
}
}
informListeners(customActions, InstallerListener.AFTER_DIR, dest, pf, null);
}
if (dest == null)
{
handler.emitError("Error creating directories", "Could not create directory\n" + path);
handler.stopAction();
return (false);
}
return (true);
}
// CUSTOM ACTION STUFF -------------- end -----------------
/**
* Returns whether an interrupt request should be discarded or not.
*
* @return Returns the discard interrupt flag
*/
public static synchronized boolean isDiscardInterrupt()
{
return discardInterrupt;
}
/**
* Sets the discard interrupt flag.
*
* @param di the discard interrupt flag to set
*/
public static synchronized void setDiscardInterrupt(boolean di)
{
discardInterrupt = di;
setInterruptDesired(false);
}
/**
* Returns the interrupt desired state.
*
* @return the interrupt desired state
*/
public static boolean isInterruptDesired()
{
return interruptDesired;
}
/**
* @param interruptDesired The interrupt desired flag to set
*/
private static void setInterruptDesired(boolean interruptDesired)
{
UnpackerBase.interruptDesired = interruptDesired;
}
/**
* Puts the uninstaller.
*
* @throws Exception Description of the Exception
*/
protected void putUninstaller() throws Exception
{
String uninstallerCondition = idata.info.getUninstallerCondition();
if ((uninstallerCondition != null) &&
(uninstallerCondition.length() > 0) &&
!this.rules.isConditionTrue(uninstallerCondition)){
Debug.log("Uninstaller has a condition (" + uninstallerCondition + ") which is not fulfilled.");
Debug.log("Skipping creation of uninstaller.");
return;
}
// get the uninstaller base, returning if not found so that
// idata.uninstallOutJar remains null
InputStream[] in = new InputStream[2];
in[0] = UnpackerBase.class.getResourceAsStream("/res/IzPack.uninstaller");
if (in[0] == null)
{
return;
}
// The uninstaller extension is facultative; it will be exist only
// if a native library was marked for uninstallation.
in[1] = UnpackerBase.class.getResourceAsStream("/res/IzPack.uninstaller-ext");
// Me make the .uninstaller directory
String dest = IoHelper.translatePath(idata.info.getUninstallerPath(), vs);
String jar = dest + File.separator + idata.info.getUninstallerName();
File pathMaker = new File(dest);
pathMaker.mkdirs();
// We log the uninstaller deletion information
udata.setUninstallerJarFilename(jar);
udata.setUninstallerPath(dest);
// We open our final jar file
FileOutputStream out = new FileOutputStream(jar);
// Intersect a buffer else byte for byte will be written to the file.
BufferedOutputStream bos = new BufferedOutputStream(out);
ZipOutputStream outJar = new ZipOutputStream(bos);
idata.uninstallOutJar = outJar;
outJar.setLevel(9);
udata.addFile(jar, true);
// We copy the uninstallers
HashSet<String> doubles = new HashSet<String>();
for (InputStream anIn : in)
{
if (anIn == null)
{
continue;
}
ZipInputStream inRes = new ZipInputStream(anIn);
ZipEntry zentry = inRes.getNextEntry();
while (zentry != null)
{
// Puts a new entry, but not twice like META-INF
if (!doubles.contains(zentry.getName()))
{
doubles.add(zentry.getName());
outJar.putNextEntry(new ZipEntry(zentry.getName()));
// Byte to byte copy
int unc = inRes.read();
while (unc != -1)
{
outJar.write(unc);
unc = inRes.read();
}
// Next one please
inRes.closeEntry();
outJar.closeEntry();
}
zentry = inRes.getNextEntry();
}
inRes.close();
}
// Should we relaunch with privileges?
if (idata.info.isPrivilegedExecutionRequired())
{
outJar.putNextEntry(new ZipEntry("exec-admin"));
outJar.closeEntry();
}
// We put the langpack
InputStream in2 = Unpacker.class.getResourceAsStream("/langpacks/" + idata.localeISO3 + ".xml");
outJar.putNextEntry(new ZipEntry("langpack.xml"));
int read = in2.read();
while (read != -1)
{
outJar.write(read);
read = in2.read();
}
outJar.closeEntry();
}
/**
* Adds additional unistall data to the uninstall data object.
*
* @param udata unistall data
* @param customData array of lists of custom action data like uninstaller listeners
*/
protected void handleAdditionalUninstallData(UninstallData udata, List[] customData)
{
// Handle uninstall libs
udata.addAdditionalData("__uninstallLibs__", customData[AutomatedInstallData.UNINSTALLER_LIBS_INDEX]);
// Handle uninstaller listeners
udata.addAdditionalData("uninstallerListeners", customData[AutomatedInstallData.UNINSTALLER_LISTENER_INDEX]);
// Handle uninstaller jars
udata.addAdditionalData("uninstallerJars", customData[AutomatedInstallData.UNINSTALLER_JARS_INDEX]);
}
public abstract void run();
/**
* @param updatechecks
*/
protected void performUpdateChecks(ArrayList<UpdateCheck> updatechecks)
{
ArrayList<RE> include_patterns = new ArrayList<RE>();
ArrayList<RE> exclude_patterns = new ArrayList<RE>();
RECompiler recompiler = new RECompiler();
this.absolute_installpath = new File(idata.getInstallPath()).getAbsoluteFile();
// at first, collect all patterns
for (UpdateCheck uc : updatechecks)
{
if (uc.includesList != null)
{
include_patterns.addAll(preparePatterns(uc.includesList, recompiler));
}
if (uc.excludesList != null)
{
exclude_patterns.addAll(preparePatterns(uc.excludesList, recompiler));
}
}
// do nothing if no update checks were specified
if (include_patterns.size() == 0)
{
return;
}
// now collect all files in the installation directory and figure
// out files to check for deletion
// use a treeset for fast access
TreeSet<String> installed_files = new TreeSet<String>();
for (String fname : this.udata.getInstalledFilesList())
{
File f = new File(fname);
if (!f.isAbsolute())
{
f = new File(this.absolute_installpath, fname);
}
installed_files.add(f.getAbsolutePath());
}
// now scan installation directory (breadth first), contains Files of
// directories to scan
// (note: we'll recurse infinitely if there are circular links or
// similar nasty things)
Stack<File> scanstack = new Stack<File>();
// contains File objects determined for deletion
ArrayList<File> files_to_delete = new ArrayList<File>();
try
{
scanstack.add(absolute_installpath);
while (!scanstack.empty())
{
File f = scanstack.pop();
File[] files = f.listFiles();
if (files == null)
{
throw new IOException(f.getPath() + "is not a directory!");
}
for (File newf : files)
{
String newfname = newf.getPath();
// skip files we just installed
if (installed_files.contains(newfname))
{
continue;
}
if (fileMatchesOnePattern(newfname, include_patterns)
&& (!fileMatchesOnePattern(newfname, exclude_patterns)))
{
files_to_delete.add(newf);
}
if (newf.isDirectory())
{
scanstack.push(newf);
}
}
}
}
catch (IOException e)
{
this.handler.emitError("error while performing update checks", e.toString());
}
for (File f : files_to_delete)
{
if (!f.isDirectory())
// skip directories - they cannot be removed safely yet
{
// this.handler.emitNotification("deleting " + f.getPath());
f.delete();
}
}
}
/**
* Writes information about the installed packs and the variables at
* installation time.
*
* @throws IOException
* @throws ClassNotFoundException
*/
public void writeInstallationInformation() throws IOException, ClassNotFoundException
{
if (!idata.info.isWriteInstallationInformation())
{
Debug.trace("skip writing installation information");
return;
}
Debug.trace("writing installation information");
String installdir = idata.getInstallPath();
List installedpacks = new ArrayList(idata.selectedPacks);
File installationinfo = new File(installdir + File.separator + AutomatedInstallData.INSTALLATION_INFORMATION);
if (!installationinfo.exists())
{
Debug.trace("creating info file" + installationinfo.getAbsolutePath());
installationinfo.createNewFile();
}
else
{
Debug.trace("installation information found");
// read in old information and update
FileInputStream fin = new FileInputStream(installationinfo);
ObjectInputStream oin = new ObjectInputStream(fin);
List packs = (List) oin.readObject();
for (Object pack1 : packs)
{
Pack pack = (Pack) pack1;
installedpacks.add(pack);
}
oin.close();
fin.close();
}
FileOutputStream fout = new FileOutputStream(installationinfo);
ObjectOutputStream oout = new ObjectOutputStream(fout);
oout.writeObject(installedpacks);
/*
int selectedpackscount = idata.selectedPacks.size();
for (int i = 0; i < selectedpackscount; i++)
{
Pack pack = (Pack) idata.selectedPacks.get(i);
oout.writeObject(pack);
}
*/
oout.writeObject(idata.variables);
Debug.trace("done.");
oout.close();
fout.close();
}
protected File getAbsolutInstallSource() throws MalformedURLException
{
if (absolutInstallSource == null)
{
URL url = getClass().getResource("/info");
if (url.getPath().startsWith("file:"))
{
url = new URL(url.getFile());
}
absolutInstallSource = new File(url.getFile()).getAbsoluteFile().getParentFile();
if (absolutInstallSource.getAbsolutePath().endsWith("!"))
{
absolutInstallSource = absolutInstallSource.getParentFile();
}
}
return absolutInstallSource;
}
}