/**
* Copyright 2012 Andrew Okin
*
* 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 forkk.multimc.data;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.Scanner;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import forkk.multimc.compat.OS;
import forkk.multimc.compat.OSUtils;
import forkk.multimc.data.exceptions.InstanceInitException;
import forkk.multimc.data.exceptions.InstanceSaveException;
import forkk.multimc.settings.AppSettings;
public class Instance
{
// Constants
/**
* Invalid characters that aren't allowed in an instance's name.
*/
public static final String InvalidNameChars = "<>\n\\/&.";
/**
* The name of the file used to store an instance's information
*/
public static final String InstanceDataFileName = "instance.xml";
// Methods
public Instance(String name, String rootDir) throws InstanceInitException
{
Init(rootDir);
setName(name);
}
public Instance(String rootDir) throws InstanceInitException
{
Init(rootDir);
}
private void Init(String rootDir) throws InstanceInitException
{
this.rootDir = rootDir;
if (!new File(rootDir).exists())
{
new File(rootDir).mkdir();
}
if (!new File(rootDir).exists())
{
throw new InstanceInitException("Failed to " +
"initialize instance because it's root directory could " +
"not be created.");
}
try
{
autosave = true;
installTimes = loadInstallTimes();
docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
if (getInstDataFile().exists())
xmlDoc = docBuilder.parse(getInstDataFile());
else
xmlDoc = docBuilder.newDocument();
} catch (ParserConfigurationException e)
{
e.printStackTrace();
throw new InstanceInitException("Failed to load the instance " +
"because the XML parser was misconfigured.");
} catch (SAXException e)
{
e.printStackTrace();
throw new InstanceInitException("Failed to parse the instance's " +
"XML file. " + e.getMessage());
} catch (IOException e)
{
e.printStackTrace();
throw new InstanceInitException("Unknown IO exception when " +
"loading instance. " + e.getMessage());
}
}
// Fields
/**
* The XML document
*/
Document xmlDoc;
DocumentBuilder docBuilder;
/**
* The instance's root directory
*/
String rootDir;
/**
* If true, the XML document will save automatically.
*/
boolean autosave;
/**
* When using the symbolic link launch method, this will keep track of what
* the original .minecraft folder has been renamed to.
*/
File origMCTemp;
/**
* The instance's process
*/
Process instProc;
// boolean symlinkLaunch;
public void Launch()
{
// this.symlinkLaunch = symlinkLaunch;
String homeParam = this.getRootDir().getAbsolutePath();
homeParam = (homeParam.contains(" ")? "\"" + homeParam + "\"" : homeParam);
ProcessBuilder mcProcBuild = new ProcessBuilder(
new File(new File(System.getProperty("java.home"), "bin"), "java").toString(),
"-Duser.home=" + homeParam,
"-Xms" + AppSettings.getInitialMemAlloc() + "m",
"-Xmx" + AppSettings.getMaxMemAlloc() + "m",
"-cp",
AppSettings.getLauncherFilename(),
"net.minecraft.LauncherFrame"
);
String cmdString = "";
for (String str : mcProcBuild.command())
{
cmdString += str + " ";
}
System.out.println("Launching with command: " + cmdString);
if (OSUtils.getCurrentOS() == OS.WINDOWS)
{
mcProcBuild.environment().put("APPDATA", rootDir);
}
// switch (OSUtils.getCurrentOS())
// {
// case WINDOWS:
// if (!symlinkLaunch)
// {
// mcProcBuild.environment().put("APPDATA", rootDir);
// break;
// }
//
// case MAC:
// case LINUX:
// default:
// symlinkLaunch = true;
// SymlinkLaunchPrep(mcProcBuild);
// break;
// }
try
{
//mcProcBuild.inheritIO();
instProc = mcProcBuild.start();
new Thread(new Runnable()
{
@Override
public void run()
{
Scanner scan = new Scanner(instProc.getInputStream());
while (isRunning() || scan.hasNextLine())
{
if (scan.hasNextLine())
System.out.println("Instance: " + scan.nextLine());
try { Thread.sleep(250); } catch (InterruptedException e) { }
}
onProcExit();
}
}).start();
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Called when Minecraft closes
*/
public void onProcExit()
{
System.out.println("Process quit.");
// if (symlinkLaunch)
// SymlinkLaunchEnd();
}
public boolean isRunning()
{
try
{
instProc.exitValue();
} catch (IllegalThreadStateException e)
{
return true;
}
return false;
}
public Process getProcess()
{
return instProc;
}
/*
/**
* Launches the instance using symbolic links
*/
/*
private void SymlinkLaunchPrep(ProcessBuilder mcProcBuild)
{
File originalMC = OSUtils.getMinecraftDir();
origMCTemp = getAvailableMCTemp();
System.out.println(String.format("Renaming %1$s to %2$s", originalMC, origMCTemp));
originalMC.renameTo(getAvailableMCTemp());
File link = OSUtils.getMinecraftDir().toPath();
File linkTarget = getMCDir().getAbsoluteFile();
if (!linkTarget.exists())
linkTarget.mkdirs();
System.out.println(String.format("Creating link at %1$s to %2$s", link, linkTarget));
try
{
Files.createSymbolicLink(link, linkTarget.toPath());
} catch (UnsupportedOperationException e)
{
// TODO: handle exception
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Cleans up after a symbolic link launch. Does nothing if origMCTemp is
* null or doesn't exist.
*/
/*
private void SymlinkLaunchEnd()
{
if (Files.isSymbolicLink(OSUtils.getMinecraftDir().toPath()))
{
OSUtils.getMinecraftDir().delete();
}
if (origMCTemp != null && origMCTemp.exists())
{
origMCTemp.renameTo(OSUtils.getMinecraftDir());
}
}
/**
* @return An available temporary file for the original .minecraft folder
* that is going to be replaced with a symbolic link.
*/
/*
private File getAvailableMCTemp()
{
final String mcTNBase = ".tempMC";
File mcParent = new File(OSUtils.getMinecraftDir().getParent());
File mcTemp = new File(mcParent, mcTNBase);
if (mcTemp.exists())
{
for (int i = 0; (i < 99999999 && mcTemp.exists()); i++)
{
mcTemp = new File(mcParent, mcTNBase + "-" + i);
}
}
return mcTemp;
}
*/
// XML Stuff
private void AutoSave()
{
if (autosave)
{
try
{
Save();
} catch (InstanceSaveException e)
{
e.printStackTrace();
return;
}
}
}
/**
* Saves the instance's XML document
* @throws InstanceSaveException If the instance fails to save for some reason
*/
public void Save() throws InstanceSaveException
{
try
{
if (!getRootDir().exists())
getRootDir().mkdirs();
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
if (!getInstDataFile().exists())
getInstDataFile().createNewFile();
StreamResult result = new StreamResult(getInstDataFile());
DOMSource source = new DOMSource(xmlDoc);
transformer.transform(source, result);
} catch (TransformerConfigurationException e)
{
e.printStackTrace();
throw new InstanceSaveException("Instance failed to save because " +
"the XML transformer was misconfigured.", this);
} catch (TransformerFactoryConfigurationError e)
{
e.printStackTrace();
throw new InstanceSaveException("Instance failed to save because " +
"the XML transformer factory was misconfigured.", this);
} catch (TransformerException e)
{
e.printStackTrace();
throw new InstanceSaveException("Instance failed to save because " +
"the XML transformer failed.", this);
} catch (IOException e)
{
e.printStackTrace();
throw new InstanceSaveException("Couldn't create instance data " +
"file.", this);
}
}
private Node getRootNode()
{
final String name = "instance";
NodeList nodes = xmlDoc.getElementsByTagName(name);
if (nodes.getLength() <= 0)
{
return xmlDoc.appendChild(xmlDoc.createElement(name));
}
else
{
return nodes.item(0);
}
}
/**
* Returns the XML element with the given name. The element will be created
* if it doesn't exist
* @param element the name of the element to get
* @param parent the name of the element's parent
* @return the XML node
*/
private Node getXmlNode(String element)
{
return getXmlNode(element, null);
}
/**
* Returns the XML element with the given name. The element will be created
* if it doesn't exist
* @param element the name of the element to get
* @param parent the name of the element's parent
* @return the XML node
*/
private Node getXmlNode(String element, Element parent)
{
return getXmlNode(element, parent, null);
}
/**
* Returns the XML element with the given name. The element will be created
* if it doesn't exist
* @param element the name of the element to get
* @param parent the name of the element's parent
* @param defValue the default value of the new node
* @return the XML node
*/
private Node getXmlNode(String element, Element parent, String defValue)
{
if (parent == null)
parent = (Element) getRootNode();
NodeList nodes = parent.getElementsByTagName(element);
if (nodes.getLength() <= 0)
{
if (defValue == null)
return parent.appendChild(xmlDoc.createElement(element));
else
{
Element newElement = xmlDoc.createElement(element);
newElement.setTextContent(defValue);
return parent.appendChild(newElement);
}
}
else
{
return nodes.item(0);
}
}
// File install times
private static String installTimesFileName = "ModFileInstallTimes";
Properties installTimes;
private Properties loadInstallTimes()
{
Properties props = new Properties();
try
{
props.load(new FileInputStream(new File(getRootDir(), installTimesFileName)));
} catch (FileNotFoundException e)
{
} catch (IOException e)
{
e.printStackTrace();
}
return props;
}
private void saveInstallTimes()
{
try
{
installTimes.store(new FileOutputStream(
new File(getRootDir(), installTimesFileName)),
"DO NOT EDIT THIS FILE! It keeps track of when you " +
"installed each of your mods so that MultiMC knows " +
"which order to add them to minecraft.jar. Editing " +
"this file could cause problems.");
} catch (FileNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public Long getInstallTime(File file)
{
Object value = installTimes.get(file.getAbsolutePath());
if (value == null)
return file.lastModified();
try
{
return Long.parseLong(value.toString());
} catch (NumberFormatException e)
{
return file.lastModified();
}
}
public void setInstallTime(File file, Long time)
{
file.setLastModified(time);
installTimes.put(file.getAbsolutePath(), time.toString());
saveInstallTimes();
}
public void recursiveSetInstallTime(File file, long time)
{
if (file.isDirectory())
{
for (File f : file.listFiles())
{
recursiveSetInstallTime(f, time);
}
}
else if (file.isFile())
{
// file.setLastModified(time);
setInstallTime(file, time);
}
}
// XML Values
public String getName() { return getXmlNode("name").getTextContent(); }
public void setName(String v) { getXmlNode("name").setTextContent(v); AutoSave(); }
public String getIconKey() { return getXmlNode("iconKey").getTextContent(); }
public void setIconKey(String v) { getXmlNode("iconKey").setTextContent(v); AutoSave(); }
public String getNotes() { return getXmlNode("notes").getTextContent(); }
public void setNotes(String v) { getXmlNode("notes").setTextContent(v); AutoSave(); }
// Directories
/**
* @return The instance's root directory
*/
public File getRootDir() { return new File(rootDir); }
/**
* @return The directory where mods will be stored
*/
public File getInstMods() { return new File(rootDir, "instMods"); }
/**
* @return The instance's .minecraft folder
*/
public File getMCDir() { return new File(rootDir, ".minecraft"); }
/**
* @return ModLoader's folder (.minecraft\mods)
*/
public File getModLoaderDir() { return new File(getMCDir(), "mods"); }
/**
* @return The instance's bin folder (.minecraft\bin)
*/
public File getBinDir() { return new File(getMCDir(), "bin"); }
/**
* @return The texture packs folder (.minecraft\texturepacks)
*/
public File getTexturePackDir() { return new File(getMCDir(), "texturepacks"); }
// Files
/**
* @return The instance's data file
*/
public File getInstDataFile() { return new File(getRootDir(), InstanceDataFileName); }
}