/*
* DataInstaller.java
* Copyright 2007 (C) James Dempsey
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Created on 22/12/2007
*
* $Id$
*/
package pcgen.gui2.dialog;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.filechooser.FileFilter;
import pcgen.cdom.enumeration.Destination;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.StringKey;
import pcgen.core.InstallableCampaign;
import pcgen.core.utils.CoreUtility;
import pcgen.core.utils.MessageType;
import pcgen.core.utils.ShowMessageDelegate;
import pcgen.gui2.tools.CommonMenuText;
import pcgen.gui2.tools.Icons;
import pcgen.gui2.tools.Utility;
import pcgen.persistence.PersistenceLayerException;
import pcgen.persistence.lst.InstallLoader;
import pcgen.system.ConfigurationSettings;
import pcgen.system.FacadeFactory;
import pcgen.system.LanguageBundle;
import pcgen.system.PCGenSettings;
import pcgen.util.Logging;
/**
* {@code DataInstaller} is responsible for managing the installation of
* a data set including the selection of the set and the install options.
*
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class DataInstaller extends JFrame
{
/** Version for serialisation */
private static final long serialVersionUID = -7429544164441235718L;
/**
* Filter class to only display potential zip format data sets.
*/
private final class DataPackFilter extends FileFilter
{
/* (non-Javadoc)
* @see javax.swing.filechooser.FileFilter#accept(java.io.File)
*/
@Override
public boolean accept(File f)
{
if (f.isDirectory())
{
return true;
}
final String nameLc = f.getName().toLowerCase();
return nameLc.endsWith(".zip") || nameLc.endsWith(".pcz");
}
/* (non-Javadoc)
* @see javax.swing.filechooser.FileFilter#getDescription()
*/
@Override
public String getDescription()
{
return "Data Sets (*.pcz,*.zip)";
}
}
/**
* The listener for receiving and processing action events from installer
* buttons.
*/
private final class InstallerButtonListener implements ActionListener
{
/**
* Gets the currently selected destination.
*
* @return the selected destination
*/
private Destination getSelectedDestination()
{
if (locDataButton.isSelected())
{
return Destination.DATA;
}
if (locVendorDataButton.isSelected())
{
return Destination.VENDORDATA;
}
if (locHomebrewDataButton.isSelected())
{
return Destination.HOMEBREWDATA;
}
return null;
}
/**
* Install a data set (campaign) into the current PCGen install.
*
* @param dataSet the data set (campaign) to be installed.
* @param dest The location the data is to be installed to.
*
* @return true, if install data source
*/
private boolean installDataSource(File dataSet, Destination dest)
{
// Get the directory the data is to be stored in
if (dataSet == null)
{
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diDataSetNotSelected"), TITLE,
MessageType.ERROR);
return false;
}
if (dest == null)
{
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diDataFolderNotSelected"), TITLE,
MessageType.ERROR);
return false;
}
File destDir;
switch (dest)
{
case VENDORDATA:
destDir = new File(PCGenSettings.getVendorDataDir());
break;
case HOMEBREWDATA:
destDir = new File(PCGenSettings.getHomebrewDataDir());
break;
case DATA:
default:
destDir = new File(ConfigurationSettings.getPccFilesDir());
break;
}
// Check chosen dir exists
if (!destDir.exists())
{
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diDataFolderNotExist", destDir
.getAbsoluteFile()), TITLE, MessageType.ERROR);
return false;
}
// Scan for non standard files and files that would be overwritten
List<String> directories = new ArrayList<>();
List<String> files = new ArrayList<>();
if (!populateFileAndDirLists(dataSet, directories, files))
{
return false;
}
if (!checkNonStandardOK(files))
{
return false;
}
if (!checkOverwriteOK(files, destDir))
{
return false;
}
if (!createDirectories(directories, destDir))
{
return false;
}
// Navigate through the zip file, processing each file
return createFiles(dataSet, destDir, files);
}
/* (non-Javadoc)
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent actionEvent)
{
JButton source = (JButton) actionEvent.getSource();
if (source == null)
{
// Do nothing
}
else if (source.equals(closeButton))
{
setVisible(false);
dispose();
}
else if (source.equals(selectButton))
{
JFileChooser chooser =
new JFileChooser(currFolder);
chooser.setDialogTitle(LanguageBundle
.getString("in_diChooserTitle")); //$NON-NLS-1$
chooser.setFileFilter(new DataPackFilter());
int result = chooser.showOpenDialog(DataInstaller.this);
if (result != JFileChooser.APPROVE_OPTION)
{
return;
}
File dataset = chooser.getSelectedFile();
currFolder = dataset.getParentFile();
readDataSet(dataset);
}
else if (source.equals(installButton))
{
if (installDataSource(currDataSet, getSelectedDestination()))
{
//PCGen_Frame1.getInst().getMainSource().refreshCampaigns();
//TODO: Refresh the data cleanly.
// PersistenceManager.getInstance().refreshCampaigns();
// FacadeFactory.refresh();
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diInstalled", campaign //$NON-NLS-1$
.getDisplayName()), TITLE, MessageType.INFORMATION);
}
}
}
/**
* Read data set.
*
* @param dataSet the data set
*
* @return true, if successful
*/
private boolean readDataSet(File dataSet)
{
// Open the ZIP file
try (ZipFile in = new ZipFile(dataSet))
{
// Get the install file in a case insensitive manner
ZipEntry installEntry = null;
@SuppressWarnings("rawtypes")
Enumeration entries = in.entries();
while (entries.hasMoreElements())
{
ZipEntry entry = (ZipEntry) entries.nextElement();
if (entry.getName().equalsIgnoreCase("install.lst"))
{
installEntry = entry;
break;
}
}
if (installEntry == null)
{
// Report that it isn't a valid data set
Logging.errorPrint("File " + dataSet
+ " is not a valid datsset - no Install.lst file");
ShowMessageDelegate.showMessageDialog(
LanguageBundle.getFormattedString("in_diNoInstallFile",
dataSet.getName()), TITLE, MessageType.WARNING);
in.close();
return false;
}
// Parse the install file
InputStream inStream = in.getInputStream(installEntry);
BufferedReader reader
= new BufferedReader(new InputStreamReader(inStream, "UTF-8")); //$NON-NLS-1$
StringBuilder installInfo = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
installInfo.append(line).append("\n");
}
final InstallLoader loader = new InstallLoader();
loader.loadLstString(null, dataSet.toURI(), installInfo.toString());
campaign = loader.getCampaign();
in.close();
}
catch (IOException e)
{
// Report the error
Logging.errorPrint("Failed to read data set " + dataSet
+ " due to ", e);
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diBadDataSet", dataSet), TITLE,
MessageType.ERROR);
return false;
}
catch (PersistenceLayerException e)
{
Logging.errorPrint("Failed to parse data set " + dataSet
+ " due to ", e);
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diBadDataSet", dataSet), TITLE,
MessageType.ERROR);
return false;
}
// Validate that the campaign is compatible with our version
if (campaign.getSafe(StringKey.MINDEVVER) != null
&& !CoreUtility.isPriorToCurrent(campaign.getSafe(StringKey.MINDEVVER)))
{
if (CoreUtility.isCurrMinorVer(campaign.getSafe(StringKey.MINDEVVER)))
{
Logging.errorPrint("Dataset " + campaign.getDisplayName()
+ " needs at least PCGen version "
+ campaign.getSafe(StringKey.MINDEVVER)
+ " to run. It could not be installed.");
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diVersionTooOldDev", campaign.getSafe(StringKey.MINDEVVER), campaign.getSafe(StringKey.MINVER)), TITLE,
MessageType.WARNING);
return false;
}
}
if (campaign.getSafe(StringKey.MINVER) != null
&& !CoreUtility.isPriorToCurrent(campaign.getSafe(StringKey.MINVER)))
{
Logging.errorPrint("Dataset " + campaign.getDisplayName()
+ " needs at least PCGen version " + campaign.getSafe(StringKey.MINVER)
+ " to run. It could not be installed.");
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diVersionTooOld", campaign.getSafe(StringKey.MINVER)),
TITLE, MessageType.WARNING);
return false;
}
// Display the info
dataSetSel.setText(dataSet.getAbsolutePath());
dataSetDetails.setText(FacadeFactory.getCampaignInfoFactory().getHTMLInfo(campaign));
if (campaign.get(ObjectKey.DESTINATION) == null)
{
locDataButton.setSelected(false);
locVendorDataButton.setSelected(false);
locHomebrewDataButton.setSelected(false);
}
else
{
switch (campaign.get(ObjectKey.DESTINATION))
{
case DATA:
locDataButton.setSelected(true);
break;
case VENDORDATA:
locVendorDataButton.setSelected(true);
break;
case HOMEBREWDATA:
locHomebrewDataButton.setSelected(true);
break;
}
}
currDataSet = dataSet;
toFront();
return true;
}
}
/** The name of the OUTPUTSHEETS folder. */
private static final String OUTPUTSHEETS_FOLDER = "outputsheets/";
/** The name of the DATA folder. */
private static final String DATA_FOLDER = "data/";
/** The standard window title. */
private static final String TITLE = LanguageBundle.getString("in_dataInstaller");
/** The component to display the path of the selected data set. */
private JTextField dataSetSel;
/** The select button. */
private JButton selectButton;
/** The data set detail display component. */
private JEditorPane dataSetDetails;
/** The button for the data location. */
private JRadioButton locDataButton;
/** The button for the vendor data location. */
private JRadioButton locVendorDataButton;
/** The button for the homebrew data location. */
private JRadioButton locHomebrewDataButton;
/** The install button. */
private JButton installButton;
/** The close button. */
private JButton closeButton;
/** The listener. */
private ActionListener listener = new InstallerButtonListener();
/** The campaign. */
private InstallableCampaign campaign;
/** The current data set. */
private File currDataSet;
/** The current folder */
private File currFolder;
/**
* Instantiates a new data installer.
*
*/
public DataInstaller()
{
currFolder = new File(System.getProperty("user.dir"));
initComponents();
setIconImage(Icons.PCGenApp.getImageIcon().getImage());
Utility.centerComponent(this, false);
}
/**
* Check for any non standard files being installed and check with the
* user if there are. Note if the user says no to installing the non
* standard files they will be removed from the file list
*
* @param files the names of the files being installed.
*
* @return Should the install process continue
*/
private boolean checkNonStandardOK(Collection<String> files)
{
Collection<String> nonStandardFiles = new ArrayList<>();
for (String filename : files)
{
if (!filename.toLowerCase().startsWith(DATA_FOLDER)
&& !filename.toLowerCase().startsWith(OUTPUTSHEETS_FOLDER))
{
nonStandardFiles.add(filename);
}
}
if (!nonStandardFiles.isEmpty())
{
StringBuilder msg = new StringBuilder();
for (String filename : nonStandardFiles)
{
msg.append(' ').append(filename).append("\n");
}
DIWarningDialog dialog =
new DIWarningDialog(this, msg.toString(), LanguageBundle
.getFormattedString("in_diNonStandardFiles"));
dialog.setVisible(true);
int result = dialog.getResponse();
if (result == JOptionPane.CANCEL_OPTION)
{
return false;
}
if (result == JOptionPane.NO_OPTION)
{
for (String filename : nonStandardFiles)
{
files.remove(filename);
}
}
}
return true;
}
/**
* Check for any files that would be overwritten and confirm it is ok
* with the user.
*
* @param files the names of the files being installed.
* @param destDir the destination data directory
*
* @return true, if successful
*/
private boolean checkOverwriteOK(Collection<String> files, File destDir)
{
Collection<String> existingFiles = new ArrayList<>();
Collection<String> existingFilesCorr = new ArrayList<>();
for (String filename : files)
{
String correctedFilename = correctFileName(destDir, filename);
if (new File(correctedFilename).exists())
{
existingFiles.add(filename);
existingFilesCorr.add(correctedFilename);
}
}
if (!existingFiles.isEmpty())
{
StringBuilder msg = new StringBuilder();
for (String filename : existingFilesCorr)
{
msg.append(' ').append(filename).append("\n");
}
DIWarningDialog dialog =
new DIWarningDialog(this, msg.toString(), LanguageBundle
.getFormattedString("in_diOverwriteFiles"));
dialog.setVisible(true);
int result = dialog.getResponse();
if (result == JOptionPane.CANCEL_OPTION)
{
return false;
}
if (result == JOptionPane.NO_OPTION)
{
for (String filename : existingFiles)
{
files.remove(filename);
}
}
}
return true;
}
/**
* Copy the contents of the input stream to the output stream. Used
* here to write the zipped file to the install location.
*
* @param in the input stream
* @param out the output stream
*
* @throws IOException Signals that an I/O exception has occurred.
*/
private void copyInputStream(InputStream in, OutputStream out)
throws IOException
{
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) >= 0)
{
out.write(buffer, 0, len);
}
in.close();
out.close();
}
/**
* Correct the file name to account for the selected data directory and
* preference based folder locations such as output sheets.
*
* @param destDir the destination data directory
* @param fileName the file name to be corrected.
*
* @return the corrected file name.
*/
private String correctFileName(File destDir, String fileName)
{
if (fileName.toLowerCase().startsWith(DATA_FOLDER))
{
fileName = destDir.getAbsolutePath() + fileName.substring(4);
}
else if (fileName.toLowerCase().startsWith(OUTPUTSHEETS_FOLDER))
{
fileName =
new File(ConfigurationSettings.getOutputSheetsDir())
.getAbsolutePath() + fileName.substring(12);
}
return fileName;
}
/**
* Creates the directories needed by the installer, where they
* do not already exist.
*
* @param directories the directories
* @param destDir the destination data directory
*
* @return true, if successful
*/
private boolean createDirectories(Iterable<String> directories, File destDir)
{
for (String dirname : directories)
{
String corrDirname = correctFileName(destDir, dirname);
File dir = new File(corrDirname);
if (!dir.exists())
{
Logging.log(Logging.INFO, "Creating directory: " + dir);
if (!dir.mkdirs())
{
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diDirNotCreated", dir
.getAbsoluteFile()), TITLE, MessageType.ERROR);
return false;
}
}
}
return true;
}
/**
* Creates the files in the archive, as filtered by previous user actions.
*
* @param dataSet the data set (campaign) to be installed.
* @param destDir the destination data directory
* @param files the list of file names
*
* @return true, if all files created ok
*/
private boolean createFiles(File dataSet, File destDir, Iterable<String> files)
{
String corrFilename = "";
try (ZipFile in = new ZipFile(dataSet)) {
for (String filename : files) {
ZipEntry entry = in.getEntry(filename);
corrFilename = correctFileName(destDir, filename);
if (Logging.isDebugMode()) {
Logging.debugPrint("Extracting file: " + filename + " to "
+ corrFilename);
}
copyInputStream(
in.getInputStream(entry),
new BufferedOutputStream(new FileOutputStream(corrFilename)));
}
return true;
} catch (IOException e) {
// Report the error
Logging.errorPrint("Failed to read data set " + dataSet
+ " or write file " + corrFilename + " due to ", e);
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diWriteFail", corrFilename), TITLE,
MessageType.ERROR);
return false;
}
}
/**
* Build the user interface ready for display.
*/
private void initComponents()
{
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.WEST;
gbc.insets = new Insets(2, 2, 2, 2);
GridBagLayout gridbag = new GridBagLayout();
setTitle(TITLE);
setLayout(gridbag);
// Data set selection row
Utility.buildConstraints(gbc, 0, 0, 1, 1, 0.0, 0.0);
JLabel dataSetLabel = new JLabel(LanguageBundle.getString("in_diDataSet"), SwingConstants.RIGHT);
gridbag.setConstraints(dataSetLabel, gbc);
add(dataSetLabel, gbc);
Utility.buildConstraints(gbc, 1, 0, 2, 1, 1.0, 0.0);
dataSetSel = new JTextField("", SwingConstants.WEST);
dataSetSel.setEditable(false);
gridbag.setConstraints(dataSetSel, gbc);
add(dataSetSel, gbc);
Utility.buildConstraints(gbc, 3, 0, 1, 1, 0.0, 0.0);
gbc.fill = GridBagConstraints.NONE;
selectButton = new JButton();
CommonMenuText.name(selectButton, "select"); //$NON-NLS-1$
gridbag.setConstraints(selectButton, gbc);
add(selectButton, gbc);
selectButton.addActionListener(listener);
// Data set details row
Utility.buildConstraints(gbc, 0, 1, 4, 1, 1.0, 1.0);
dataSetDetails = new JEditorPane("text/html", "<html></html>");
dataSetDetails.setPreferredSize(new Dimension(400, 200));
dataSetDetails.setEditable(false);
dataSetDetails.setBackground(getBackground());
gbc.fill = GridBagConstraints.BOTH;
JScrollPane jScrollPane = new JScrollPane();
jScrollPane.setViewportView(dataSetDetails);
gridbag.setConstraints(jScrollPane, gbc);
add(jScrollPane, gbc);
// Location row
Utility.buildConstraints(gbc, 0, 2, 1, 1, 0.0, 0.0);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel locLabel = new JLabel(LanguageBundle.getString("in_diLocation"), SwingConstants.RIGHT);
gridbag.setConstraints(locLabel, gbc);
add(locLabel, gbc);
ButtonGroup exclusiveGroup = new ButtonGroup();
locDataButton =
new JRadioButton(LanguageBundle
.getString("in_diData"));
locDataButton.setToolTipText(LanguageBundle.getString("in_diData_tip"));
exclusiveGroup.add(locDataButton);
locVendorDataButton =
new JRadioButton(LanguageBundle
.getString("in_diVendorData"));
locVendorDataButton.setToolTipText(LanguageBundle
.getString("in_diVendorData_tip"));
exclusiveGroup.add(locVendorDataButton);
locHomebrewDataButton =
new JRadioButton(LanguageBundle
.getString("in_diHomebrewData"));
locHomebrewDataButton.setToolTipText(LanguageBundle
.getString("in_diHomebrewData_tip"));
exclusiveGroup.add(locHomebrewDataButton);
JPanel optionsPanel = new JPanel();
optionsPanel.add(locDataButton);
optionsPanel.add(locVendorDataButton);
optionsPanel.add(locHomebrewDataButton);
Utility.buildConstraints(gbc, 1, 2, 3, 1, 0.0, 0.0);
gridbag.setConstraints(optionsPanel, gbc);
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.WEST;
add(optionsPanel, gbc);
// Buttons row
installButton = new JButton();
CommonMenuText.name(installButton, "diInstall"); //$NON-NLS-1$
installButton.addActionListener(listener);
closeButton = new JButton();
CommonMenuText.name(closeButton, "close"); //$NON-NLS-1$
closeButton.addActionListener(listener);
JPanel buttonsPanel = new JPanel();
buttonsPanel.add(installButton);
buttonsPanel.add(closeButton);
Utility.buildConstraints(gbc, 2, 3, 2, 1, 0.0, 0.0);
gridbag.setConstraints(buttonsPanel, gbc);
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.EAST;
add(buttonsPanel, gbc);
pack();
}
/**
* Populate the lists of files and directories to be installed.
*
* @param dataSet the data set (campaign) to be installed.
* @param directories the list of directory names
* @param files the list of file names
*
* @return true, if populate file and dir lists
*/
@SuppressWarnings("rawtypes")
private boolean populateFileAndDirLists(File dataSet,
Collection<String> directories, Collection<String> files)
{
// Navigate through the zip file, processing each file
// Open the ZIP file
try (ZipFile in = new ZipFile(dataSet)) {
Enumeration entries = in.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
if (entry.isDirectory()) {
directories.add(entry.getName());
} else if (!entry.getName().equalsIgnoreCase("install.lst")) {
files.add(entry.getName());
}
}
in.close();
}
catch (IOException e)
{
// Report the error
Logging.errorPrint("Failed to read data set " + dataSet
+ " due to ", e);
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_diBadDataSet", dataSet), TITLE,
MessageType.ERROR);
return false;
}
return true;
}
}