+ {
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ protected Void doInBackground ()
+ throws Exception
+ {
+ try {
+ bundle.installBundle();
+ logger.info("\nYou can now safely exit"
+ + ", Audiveris application will be launched.\n");
+ if (Installer.hasUI()) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ "Installation completed successfully",
+ "Installation completion",
+ JOptionPane.INFORMATION_MESSAGE);
+ }
+
+ Jnlp.extensionInstallerService.installSucceeded(false);
+ } catch (Exception ex) {
+ if (Installer.hasUI()) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ "Installation has failed: \n" + ex.getMessage(),
+ "Installation completion",
+ JOptionPane.WARNING_MESSAGE);
+ }
+
+ Jnlp.extensionInstallerService.installFailed();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done ()
+ {
+ startAction.setEnabled(true);
+ stopAction.setEnabled(true);
+
+ stopAction.putValue(AbstractAction.NAME, CLOSE);
+ stopAction.putValue(
+ AbstractAction.SHORT_DESCRIPTION,
+ "Close the installer");
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/Companion.java b/src/installer/com/audiveris/installer/Companion.java
new file mode 100644
index 0000000..eebc0fa
--- /dev/null
+++ b/src/installer/com/audiveris/installer/Companion.java
@@ -0,0 +1,120 @@
+//----------------------------------------------------------------------------//
+// //
+// C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+/**
+ * Interface {@code Companion} defines a companion to install as part
+ * of Audiveris bundle.
+ * A companion does the maximum via Java code run in user mode.
+ * If some operation needs admin privilege then this operation (as small as
+ * possible) must be written as a command and appended to the bundle global
+ * command list which will be run at the end.
+ *
+ * @author Hervé Bitteur
+ */
+public interface Companion
+{
+ //~ Enumerations -----------------------------------------------------------
+
+ /** Need of Audiveris with respect to a companion. */
+ enum Need
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ /**
+ * Companion must be present.
+ */
+ MANDATORY,
+ /**
+ * Optional companion but selected.
+ */
+ SELECTED,
+ /**
+ * Optional companion not selected.
+ */
+ NOT_SELECTED;
+
+ }
+
+ /** Current installation status of a companion. */
+ enum Status
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ /**
+ * Companion is not (yet) installed.
+ */
+ NOT_INSTALLED,
+ /**
+ * Companion is being installed.
+ */
+ BEING_INSTALLED,
+ /**
+ * Companion is being uninstalled.
+ */
+ BEING_UNINSTALLED,
+ /**
+ * Companion is installed.
+ */
+ INSTALLED,
+ /**
+ * Installation has failed.
+ */
+ FAILED_TO_INSTALL,
+ /**
+ * Uninstallation has failed.
+ */
+ FAILED_TO_UNINSTALL;
+
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ /** Check whether this companion has been installed. */
+ boolean checkInstalled ();
+
+ /** Report the full description. */
+ String getDescription ();
+
+ /** Report the companion index. */
+ int getIndex ();
+
+ /** Report a weight for installation. */
+ int getInstallWeight ();
+
+ /** Get companion need. */
+ Need getNeed ();
+
+ /** Get companion installation status. */
+ Status getStatus ();
+
+ /** Report the companion title for display. */
+ String getTitle ();
+
+ /** Report a weight for uninstallation. */
+ int getUninstallWeight ();
+
+ /** Report the related view, if any. */
+ CompanionView getView ();
+
+ /** Launch installation of this companion. */
+ void install ()
+ throws Exception;
+
+ /** Check whether installation is needed. */
+ boolean isNeeded ();
+
+ /** Set companion need. */
+ void setNeed (Need need);
+
+ /** Launch de-installation of this companion. */
+ void uninstall ();
+}
diff --git a/src/installer/com/audiveris/installer/CompanionView.java b/src/installer/com/audiveris/installer/CompanionView.java
new file mode 100644
index 0000000..13b2cb2
--- /dev/null
+++ b/src/installer/com/audiveris/installer/CompanionView.java
@@ -0,0 +1,49 @@
+//----------------------------------------------------------------------------//
+// //
+// C o m p a n i o n V i e w //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import java.awt.Color;
+
+import javax.swing.JComponent;
+
+/**
+ * Interface {@code CompanionView} defines a view on a {@link
+ * Companion}
+ *
+ * @author Hervé Bitteur
+ */
+public interface CompanionView
+{
+ //~ Methods ----------------------------------------------------------------
+
+ /** Report the underlying companion. */
+ Companion getCompanion ();
+
+ /** Report the Swing component. */
+ JComponent getComponent ();
+
+ /** Refresh the view from related companion. */
+ void update ();
+
+ //~ Inner Interfaces -------------------------------------------------------
+
+ interface COLORS
+ {
+ //~ Static fields/initializers -----------------------------------------
+
+ static final Color NOT_INST = new Color(245, 230, 220);
+ static final Color INST = new Color(200, 255, 200);
+ static final Color UNUSED = new Color(220, 220, 220);
+ static final Color BEING = new Color(255, 200, 0);
+ static final Color FAILED = new Color(255, 100, 100);
+ }
+}
diff --git a/src/installer/com/audiveris/installer/CppCompanion.java b/src/installer/com/audiveris/installer/CppCompanion.java
new file mode 100644
index 0000000..fab7956
--- /dev/null
+++ b/src/installer/com/audiveris/installer/CppCompanion.java
@@ -0,0 +1,76 @@
+//----------------------------------------------------------------------------//
+// //
+// C p p C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class {@code CppCompanion} handles installation of C++ runtime.
+ *
+ * @author Hervé Bitteur
+ */
+public class CppCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final org.slf4j.Logger logger = LoggerFactory.getLogger(
+ CppCompanion.class);
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ private static final String DESC = "Audiveris Java application uses JNI "
+ + " to access Tesseract C++ software."
+ + " We thus need a specific version of"
+ + " C++ runtime.";
+
+ //~ Constructors -----------------------------------------------------------
+ //--------------//
+ // CppCompanion //
+ //--------------//
+ /**
+ * Creates a new CppCompanion object.
+ */
+ public CppCompanion ()
+ {
+ super("C++", DESC);
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 50);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // checkInstalled //
+ //----------------//
+ @Override
+ public boolean checkInstalled ()
+ {
+ status = descriptor.isCppInstalled() ? Status.INSTALLED
+ : Status.NOT_INSTALLED;
+
+ return status == Status.INSTALLED;
+ }
+
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ descriptor.installCpp();
+ }
+}
diff --git a/src/installer/com/audiveris/installer/Descriptor.java b/src/installer/com/audiveris/installer/Descriptor.java
new file mode 100644
index 0000000..864fd5d
--- /dev/null
+++ b/src/installer/com/audiveris/installer/Descriptor.java
@@ -0,0 +1,194 @@
+//----------------------------------------------------------------------------//
+// //
+// D e s c r i p t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Interface {@code Descriptor} defines the features to be provided
+ * to the Installer by any target environment (os + arch).
+ *
+ * This interface has been defined with generality in mind, but may have
+ * suffered from the bias of the initial Windows-based implementation.
+ * Hence, it still may evolve when we hit the development of implementations for
+ * Unix and for Mac.
+ *
+ * @author Hervé Bitteur
+ */
+public interface Descriptor
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** ID for company developing audiveris software. */
+ static final String COMPANY_ID = "AudiverisLtd";
+
+ /** Name for audiveris program. */
+ static final String TOOL_NAME = "audiveris";
+
+ /** Name of environment variable for tessdata parent. */
+ static final String TESSDATA_PREFIX = "TESSDATA_PREFIX";
+
+ /** Name of Tesseract folder. */
+ static final String TESSERACT_OCR = "tesseract-ocr";
+
+ /** Name of tessdata folder. */
+ static final String TESSDATA = "tessdata";
+
+ /** Minimum suitable version for Ghostscript. */
+ static final String GHOSTSCRIPT_MIN_VERSION = "9.06";
+
+ //~ Methods ----------------------------------------------------------------
+ /**
+ * Report the folder to be used for read-write configuration.
+ *
+ * @return the configuration folder
+ */
+ File getConfigFolder ();
+
+ /**
+ * Report the shell command to copy recursively source to target
+ *
+ * @param source path of source folder
+ * @param target path of target folder
+ * @return the proper shell command
+ */
+ String getCopyCommand (Path source,
+ Path target);
+
+ /**
+ * Report the folder to be used for read-write data.
+ *
+ * @return the data folder
+ */
+ File getDataFolder ();
+
+ /**
+ * Report the folder to be used for Tesseract-OCR if not yet set.
+ * If Tesseract application is installed, this folder is pointed by the
+ * environment variable TESSDATA_PREFIX, and this method is not called.
+ * If Tesseract application is not installed (and actually Audiveris does
+ * not need the application, but just the language files) then we call this
+ * method to know where we have to store the Tesseract trained data files.
+ *
+ * @return the parent folder of tessdata
+ */
+ File getDefaultTessdataPrefix ();
+
+ /**
+ * Report the shell command to delete a file
+ *
+ * @param file path of file to delete
+ * @return the proper shell command
+ */
+ String getDeleteCommand (Path file);
+
+ /**
+ * Report the shell command to create directories
+ *
+ * @param dir path to final folder
+ * @return the proper shell command
+ */
+ String getMkdirCommand (Path dir);
+
+ /**
+ * Report the shell command to set the executable flag on a file
+ *
+ * @param file path of file to set
+ * @return the proper shell command
+ */
+ String getSetExecCommand (Path file);
+
+ /**
+ * Report the collection of specific files to install
+ *
+ * @return the references of specific files for the target environment
+ */
+ List getSpecificFiles ();
+
+ /**
+ * Report a folder which can be used for temporary files created
+ * during installation.
+ * This folder is created anew when installation starts, and deleted when
+ * installation completes successfully.
+ *
+ * @return a temporary folder
+ */
+ File getTempFolder ();
+
+ /**
+ * Install the proper C++ runtime.
+ */
+ void installCpp ()
+ throws Exception;
+
+ /**
+ * Install a suitable Ghostscript.
+ */
+ void installGhostscript ()
+ throws Exception;
+
+ /**
+ * Install a suitable Tesseract.
+ */
+ void installTesseract ()
+ throws Exception;
+
+ /**
+ * Report whether the current process is run with administrator
+ * privileges.
+ *
+ * @return true if admin
+ */
+ boolean isAdmin ();
+
+ /**
+ * Check whether the proper C++ runtime is installed.
+ *
+ * @return true if installed
+ */
+ boolean isCppInstalled ();
+
+ /**
+ * Check whether a suitable Ghostscript is installed.
+ *
+ * @return true if installed
+ */
+ boolean isGhostscriptInstalled ();
+
+ /**
+ * Check whether a suitable Tesseract is installed.
+ *
+ * @return true if installed
+ */
+ boolean isTesseractInstalled ();
+
+ /**
+ * Run a shell with admin privilege on the provided commands
+ *
+ * @param asAdmin true for elevated
+ * @param commands the list of commands to run
+ * @return true if OK
+ */
+ boolean runShell (boolean asAdmin,
+ List commands)
+ throws Exception;
+
+ /**
+ * Mark the provided file as executable.
+ *
+ * @param file the file to be set
+ */
+ void setExecutable (Path file)
+ throws Exception;
+}
diff --git a/src/installer/com/audiveris/installer/DescriptorFactory.java b/src/installer/com/audiveris/installer/DescriptorFactory.java
new file mode 100644
index 0000000..de1bd44
--- /dev/null
+++ b/src/installer/com/audiveris/installer/DescriptorFactory.java
@@ -0,0 +1,104 @@
+//----------------------------------------------------------------------------//
+// //
+// D e s c r i p t o r F a c t o r y //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import com.audiveris.installer.unix.UnixDescriptor;
+import com.audiveris.installer.windows.WindowsDescriptor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+/**
+ * Class {@code DescriptorFactory} is in charge of returning the
+ * suitable Descriptor instance for the current environment (os + arch).
+ *
+ * @author Hervé Bitteur
+ */
+public class DescriptorFactory
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ DescriptorFactory.class);
+
+ /** Precise OS name. */
+ private static final String OS_NAME = System.getProperty("os.name")
+ .toLowerCase(Locale.ENGLISH);
+
+ /** Precise OS architecture. */
+ public static final String OS_ARCH = System.getProperty("os.arch")
+ .toLowerCase(Locale.ENGLISH);
+
+ /** Are we using a Linux OS?. */
+ public static final boolean LINUX = OS_NAME.startsWith("linux");
+
+ /** Are we using a Mac OS?. */
+ public static final boolean MAC_OS_X = OS_NAME.startsWith("mac os x");
+
+ /** Are we using a Windows OS?. */
+ public static final boolean WINDOWS = OS_NAME.startsWith("windows");
+
+ /** Are we using Windows on 64 bit architecture?. */
+ public static final boolean WINDOWS_64 = WINDOWS
+ && (System.getenv(
+ "ProgramFiles(x86)") != null);
+
+ /** Are we running wow (appli-32/windows-64) or pure (32/32 - 64/64)?. */
+ public static final boolean WOW = OS_ARCH.equals("x86") && WINDOWS_64;
+
+ /** THE descriptor instance. */
+ private static Descriptor descriptor;
+
+ //~ Constructors -----------------------------------------------------------
+ /** Not meant to be instantiated. */
+ private DescriptorFactory ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // geteDescriptor //
+ //----------------//
+ public static Descriptor getDescriptor ()
+ {
+ if (descriptor == null) {
+ descriptor = createDescriptor();
+ }
+
+ return descriptor;
+ }
+
+ //------------------//
+ // createDescriptor //
+ //------------------//
+ private static Descriptor createDescriptor ()
+ {
+ if (WINDOWS) {
+ logger.debug("Creating a Windows descriptor");
+
+ return new WindowsDescriptor();
+ }
+
+ if (LINUX) {
+ logger.debug("Creating a Unix descriptor");
+
+ return new UnixDescriptor();
+ }
+
+ // For all others...
+ throw new UnsupportedEnvironmentException(
+ "name: " + OS_NAME + ", arch: " + OS_ARCH);
+ }
+}
diff --git a/src/installer/com/audiveris/installer/DocCompanion.java b/src/installer/com/audiveris/installer/DocCompanion.java
new file mode 100644
index 0000000..8ae9fa5
--- /dev/null
+++ b/src/installer/com/audiveris/installer/DocCompanion.java
@@ -0,0 +1,278 @@
+//----------------------------------------------------------------------------//
+// //
+// D o c C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import javax.swing.JOptionPane;
+
+/**
+ * Class {@code DocCompanion} handles the local installation of
+ * documentation.
+ * Since it's the only mandatory general purpose companion, it also handles:
+ *
+ * the creation of the data directories
+ * ("benches", "eval", "print", "scores", "scripts").
+ * the creation of specific files (.bat and .sh)
+ *
+ *
+ * @author Hervé Bitteur
+ */
+public class DocCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ DocCompanion.class);
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ /** Tooltip. */
+ private static final String DESC =
+ "The whole Audiveris documentation is made"
+ + " available in one local browsable folder.";
+
+ /** List of sub-folders created in Data folder. */
+ private static final String[] dataFolders = new String[]{
+ "benches", "eval", "print", "scores", "scripts"};
+
+ //~ Constructors -----------------------------------------------------------
+ //--------------//
+ // DocCompanion //
+ //--------------//
+ /**
+ * Creates a new DocCompanion object.
+ */
+ public DocCompanion ()
+ {
+ super("Docs", DESC);
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 60);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // checkInstalled //
+ //----------------//
+ @Override
+ public boolean checkInstalled ()
+ {
+ if (getTargetFolder().exists()
+ && descriptor.getConfigFolder().exists()
+ && checkDirectories(descriptor.getDataFolder(), dataFolders)
+ && checkSpecificFiles()) {
+ status = Status.INSTALLED;
+ } else {
+ status = Status.NOT_INSTALLED;
+ }
+
+ return status == Status.INSTALLED;
+ }
+
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ final URI codeBase = Jnlp.basicService.getCodeBase()
+ .toURI();
+ final URL url = Utilities.toURI(
+ codeBase,
+ "resources/documentation.jar")
+ .toURL();
+ Utilities.downloadJarAndExpand(
+ "Docs",
+ url.toString(),
+ descriptor.getTempFolder(),
+ "www",
+ makeTargetFolder());
+
+ // Also, create user config directories
+ //TODO: should populate logging-config.xml, run.properties, user-actions.xml
+ createDirectories(descriptor.getConfigFolder());
+
+ // Create empty user data directories
+ createDirectories(descriptor.getDataFolder(), dataFolders);
+
+ // Install some specific files
+ installSpecificFiles();
+ }
+
+ //-----------------//
+ // getTargetFolder //
+ //-----------------//
+ @Override
+ protected File getTargetFolder ()
+ {
+ return new File(descriptor.getDataFolder(), "www");
+ }
+
+ //-------------------//
+ // createDirectories //
+ //-------------------//
+ private void createDirectories (File root,
+ String... names)
+ {
+ if (root.mkdirs()) {
+ logger.info("Created folder {}", root.getAbsolutePath());
+ }
+
+ for (String name : names) {
+ File folder = new File(root, name);
+
+ if (folder.mkdirs()) {
+ logger.info("Created folder {}", folder.getAbsolutePath());
+ }
+ }
+ }
+
+ //------------------//
+ // checkDirectories //
+ //------------------//
+ private boolean checkDirectories (File root,
+ String... names)
+ {
+ if (!root.exists()) {
+ return false;
+ }
+
+ for (String name : names) {
+ File folder = new File(root, name);
+
+ if (!folder.exists()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ //----------------------//
+ // installSpecificFiles //
+ //----------------------//
+ /**
+ * Install specific files for the current OS.
+ * For the time being, we assume that all these files are located in
+ * writable locations. If this assumption turns out to be false, we'll have
+ * to fall back to posting shell commands (Not yet implemented).
+ *
+ * @throws Exception
+ */
+ private void installSpecificFiles ()
+ throws Exception
+ {
+ // Download the global archive of specific files
+ final URI codeBase = Jnlp.basicService.getCodeBase().toURI();
+ final URL url = Utilities.toURI(codeBase, "resources/specifics.jar")
+ .toURL();
+ final String jarName = new File(url.toString()).getName();
+ final File jarFile = new File(descriptor.getTempFolder(), jarName);
+ Utilities.download(url.toString(), jarFile);
+
+ final JarFile jar = new JarFile(jarFile);
+
+ // Process each desired specific file
+ for (SpecificFile specificFile : descriptor.getSpecificFiles()) {
+ // Make sure the target folder exists
+ final Path target = Paths.get(specificFile.target);
+ final Path parent = target.getParent();
+
+ if (!Files.exists(parent)) {
+ try {
+ Files.createDirectories(parent);
+ logger.info("Created dir {}", parent);
+ } catch (IOException ex) {
+ logger.debug("Could not directly create dirs {}"
+ + ", will post command at system level", parent);
+ // Resort to shell
+ appendCommand(descriptor.getMkdirCommand(parent));
+ }
+ }
+
+ // Copy the source entry to the target file
+ ZipEntry entry = jar.getEntry(specificFile.source);
+ if (entry != null) {
+ try (InputStream is = jar.getInputStream(entry)) {
+ // May fail
+ Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
+ logger.info("Specific {} copied", target);
+ if (specificFile.isExec) {
+ try {
+ // May fail (but copy must have failed before)
+ descriptor.setExecutable(target);
+ } catch (Exception ignored) {
+ // User already warned, proceed to next file
+ }
+ }
+ } catch (IOException ex) {
+ logger.debug("Could not directly write {}"
+ + ", will post command at system level", target);
+ // Resort to local copy, then shell command
+ Path local = Paths.get(descriptor.getTempFolder().toString(),
+ target.getFileName().toString());
+ try (InputStream is = jar.getInputStream(entry)) {
+ Files.copy(is, local, StandardCopyOption.REPLACE_EXISTING);
+ logger.info("Specific {} copied locally", target);
+ // Here local and target are regular files (not folders)
+ appendCommand(descriptor.getCopyCommand(local, parent));
+ if (specificFile.isExec) {
+ appendCommand(descriptor.getSetExecCommand(target));
+ }
+ }
+ }
+ } else {
+ // We can live without these files, so warn the user
+ // but keep the installation going.
+ String msg = "No entry " + specificFile.source + "\n in " + url;
+ logger.warn(msg);
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ msg,
+ "Entry not found",
+ JOptionPane.WARNING_MESSAGE);
+ }
+ }
+ }
+
+ //--------------------//
+ // checkSpecificFiles //
+ //--------------------//
+ private boolean checkSpecificFiles ()
+ {
+ for (SpecificFile specificFile : descriptor.getSpecificFiles()) {
+ if (!new File(specificFile.target).exists()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/ExamplesCompanion.java b/src/installer/com/audiveris/installer/ExamplesCompanion.java
new file mode 100644
index 0000000..c314433
--- /dev/null
+++ b/src/installer/com/audiveris/installer/ExamplesCompanion.java
@@ -0,0 +1,85 @@
+//----------------------------------------------------------------------------//
+// //
+// E x a m p l e s C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+
+/**
+ * Class {@code ExamplesCompanion} handles the installation of a few
+ * image examples for Audiveris.
+ *
+ * @author Hervé Bitteur
+ */
+public class ExamplesCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ ExamplesCompanion.class);
+
+ private static final String DESC = "[This component is optional ]"
+ + " It installs a few image examples.";
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ //~ Constructors -----------------------------------------------------------
+ /**
+ * Creates a new ExamplesCompanion object.
+ */
+ public ExamplesCompanion ()
+ {
+ super("Examples", DESC);
+ need = Need.SELECTED;
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 105);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ final URI codeBase = Jnlp.basicService.getCodeBase()
+ .toURI();
+ final URL url = Utilities.toURI(codeBase, "resources/examples.jar")
+ .toURL();
+
+ Utilities.downloadJarAndExpand(
+ "Examples",
+ url.toString(),
+ descriptor.getTempFolder(),
+ "examples",
+ makeTargetFolder());
+ }
+
+ //-----------------//
+ // getTargetFolder //
+ //-----------------//
+ @Override
+ protected File getTargetFolder ()
+ {
+ return new File(descriptor.getDataFolder(), "examples");
+ }
+}
diff --git a/src/installer/com/audiveris/installer/Expander.java b/src/installer/com/audiveris/installer/Expander.java
new file mode 100644
index 0000000..3e5c4f3
--- /dev/null
+++ b/src/installer/com/audiveris/installer/Expander.java
@@ -0,0 +1,179 @@
+//----------------------------------------------------------------------------//
+// //
+// E x p a n d e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveStreamFactory;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.utils.IOUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Class {@code Expander} gathers methods to expand a .gz or a .tar
+ * file.
+ * These methods can be used to expand a .tar.gz archive.
+ *
+ * @author Dan Borza (at http://stackoverflow.com/users/510638/dan-borza)
+ * @author Hervé Bitteur
+ */
+public final class Expander
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ Expander.class);
+
+ /** .gz extension. */
+ private static final String GZ_EXT = ".gz";
+
+ /** .tar extension. */
+ private static final String TAR_EXT = ".tar";
+
+ //~ Constructors -----------------------------------------------------------
+ /** Not meant to be instantiated */
+ private Expander ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //--------------//
+ // streamToFile //
+ //--------------//
+ /**
+ * Store the provided input stream to the desired file.
+ *
+ * @param in the input stream. It is not closed by this method.
+ * @param outFile the desired target file
+ * @return the target file
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public static File streamToFile (final InputStream in,
+ final File outFile)
+ throws FileNotFoundException, IOException
+ {
+ final OutputStream out = new FileOutputStream(outFile);
+ IOUtils.copy(in, out);
+ out.close();
+
+ return outFile;
+ }
+
+ //--------//
+ // unGzip //
+ //--------//
+ /**
+ * Ungzip an input file into an output file.
+ *
+ * The output file is created in the output folder, having the same name
+ * as the input file, minus the '.gz' extension.
+ *
+ * @param inFile the input .gz file
+ * @param outDir the output directory file.
+ * @throws IOException
+ * @throws FileNotFoundException
+ *
+ * @return The file with the ungzipped content.
+ */
+ public static File unGzip (final File inFile,
+ final File outDir)
+ throws FileNotFoundException, IOException
+ {
+ logger.debug("Ungzipping {} to dir {}", inFile, outDir);
+
+ final String inName = inFile.getName();
+ assert inName.endsWith(GZ_EXT);
+
+ final InputStream in = new GZIPInputStream(new FileInputStream(inFile));
+ final File outFile = streamToFile(
+ in,
+ new File(
+ outDir,
+ inName.substring(0, inName.length() - GZ_EXT.length())));
+ in.close();
+
+ return outFile;
+ }
+
+ //-------//
+ // unTar //
+ //-------//
+ /**
+ * Untar an input file into an output directory.
+ *
+ * @param inFile the input .tar file
+ * @param outDir the output directory
+ * @throws IOException
+ * @throws FileNotFoundException
+ * @throws ArchiveException
+ *
+ * @return The list of files with the untared content.
+ */
+ public static List unTar (final File inFile,
+ final File outDir)
+ throws FileNotFoundException, IOException, ArchiveException
+ {
+ logger.debug("Untaring {} to dir {}", inFile, outDir);
+ assert inFile.getName()
+ .endsWith(TAR_EXT);
+
+ final List untaredFiles = new ArrayList();
+ final InputStream is = new FileInputStream(inFile);
+ final TarArchiveInputStream tis = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream(
+ "tar",
+ is);
+ ArchiveEntry entry;
+
+ while ((entry = tis.getNextEntry()) != null) {
+ final File outFile = new File(outDir, entry.getName());
+
+ if (entry.isDirectory()) {
+ logger.debug("Attempting to write output dir {}", outFile);
+
+ if (!outFile.exists()) {
+ logger.debug("Attempting to create output dir {}", outFile);
+
+ if (!outFile.mkdirs()) {
+ throw new IllegalStateException(
+ String.format(
+ "Couldn't create directory %s",
+ outFile.getAbsolutePath()));
+ }
+ }
+ } else {
+ logger.debug("Creating output file {}", outFile);
+ streamToFile(tis, outFile);
+ }
+
+ untaredFiles.add(outFile);
+ }
+
+ tis.close();
+
+ return untaredFiles;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/FileCopier.java b/src/installer/com/audiveris/installer/FileCopier.java
new file mode 100644
index 0000000..fc88794
--- /dev/null
+++ b/src/installer/com/audiveris/installer/FileCopier.java
@@ -0,0 +1,179 @@
+//----------------------------------------------------------------------------//
+// //
+// F i l e C o p i e r //
+// //
+//----------------------------------------------------------------------------//
+//
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved.
+// This software is released under the GNU General Public License.
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions.
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import static java.nio.file.FileVisitResult.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import static java.nio.file.StandardCopyOption.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Objects;
+
+/**
+ * Class {@code FileCopier} copies a collection of source files
+ * to a target.
+ *
+ * If several source files are provided, the target must be a directory.
+ * If a source file is a directory and if the {@code recursive} flag is set,
+ * then the directory content is copied recursively.
+ *
+ * @author Hervé Bitteur
+ */
+public class FileCopier
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final Logger logger = LoggerFactory.getLogger(
+ FileCopier.class);
+
+ //~ Instance fields --------------------------------------------------------
+
+ /** Sources. */
+ private final Path[] sources;
+
+ /** Target. */
+ private final Path target;
+
+ /** Recursive flag. */
+ private final boolean recursive;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //------------//
+ // FileCopier //
+ //------------//
+ /**
+ * Creates a new FileCopier object.
+ *
+ * @param sources the sources collection, perhaps empty but not null
+ * @param target the target file (perhaps a folder)
+ * @param recursive true for a recursive copy
+ */
+ public FileCopier (Path[] sources,
+ Path target,
+ boolean recursive)
+ {
+ Objects.requireNonNull(sources, "Sources cannot be null");
+
+ // Make a deep copy of sources collection
+ this.sources = new Path[sources.length];
+
+ for (int i = 0; i < sources.length; i++) {
+ this.sources[i] = sources[i];
+ }
+
+ Objects.requireNonNull(target, "Target cannot be null");
+ this.target = target;
+
+ this.recursive = recursive;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //------//
+ // copy //
+ //------//
+ /**
+ * Perform the copy.
+ */
+ public void copy ()
+ throws IOException
+ {
+ // Is target a directory?
+ boolean isDir = Files.isDirectory(target);
+
+ // Copy each source (file or folder) to target
+ for (Path source : sources) {
+ Path dest = isDir ? target.resolve(source.getFileName()) : target;
+
+ try {
+ if (recursive) {
+ Files.walkFileTree(source, new TreeCopier(source, dest));
+ } else {
+ if (Files.isDirectory(source)) {
+ logger.warn("{} is a directory", source);
+ } else {
+ Files.copy(source, dest, REPLACE_EXISTING);
+ }
+ }
+ } catch (IOException ex) {
+ logger.info(
+ "Cannot copy '" + source + "' to '" + dest + "' ex:" + ex);
+ throw ex;
+ }
+ }
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+
+ //------------//
+ // TreeCopier //
+ //------------//
+ /**
+ * TreeCopier is meant to recursively copy a tree of folders
+ * rooted at {@code source} to {@code dest} folder.
+ */
+ private class TreeCopier
+ extends SimpleFileVisitor
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private final Path source;
+ private final Path dest;
+
+ //~ Constructors -------------------------------------------------------
+
+ public TreeCopier (Path source,
+ Path dest)
+ {
+ this.source = source;
+ this.dest = dest;
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public FileVisitResult preVisitDirectory (Path dir,
+ BasicFileAttributes attrs)
+ throws IOException
+ {
+ Path newFolder = dest.resolve(source.relativize(dir));
+
+ if (Files.notExists(newFolder)) {
+ logger.debug("Creating folder {}", newFolder);
+ Files.copy(dir, newFolder, REPLACE_EXISTING);
+ } else {
+ logger.debug("Folder {} already exists.", newFolder);
+ }
+
+ return CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile (Path file,
+ BasicFileAttributes attrs)
+ throws IOException
+ {
+ final Path destPath = dest.resolve(source.relativize(file));
+ logger.debug("Copying file {} to {}", file, destPath);
+ Files.copy(file, destPath, REPLACE_EXISTING);
+
+ return CONTINUE;
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/FolderSelector.java b/src/installer/com/audiveris/installer/FolderSelector.java
new file mode 100644
index 0000000..592b151
--- /dev/null
+++ b/src/installer/com/audiveris/installer/FolderSelector.java
@@ -0,0 +1,206 @@
+//----------------------------------------------------------------------------//
+// //
+// F o l d e r S e l e c t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import com.jgoodies.forms.builder.PanelBuilder;
+import com.jgoodies.forms.layout.CellConstraints;
+import com.jgoodies.forms.layout.FormLayout;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+/**
+ * Class {@code FolderSelector} displays the default install folder,
+ * and let the user choose another one, if any.
+ *
+ * NOTA: THIS CLASS IS NOT USED FOR THE TIME BEING
+ * Since the notion of installation folder is not clear!
+ * - With Java Web Start, appli "program data" is located in Java cache
+ * - On Linux, appli "user config" and appli "user data" do not have the same
+ * parent
+ *
+ * @author Hervé Bitteur
+ */
+@Deprecated
+public class FolderSelector
+ implements ActionListener
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ FolderSelector.class);
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ //~ Instance fields --------------------------------------------------------
+ /** The containing bundle. */
+ private final Bundle bundle;
+
+ /** The swing component. */
+ private JComponent component;
+
+ /** Text field for folder path. */
+ private JTextField path = new JTextField();
+
+ //~ Constructors -----------------------------------------------------------
+ //----------------//
+ // FolderSelector //
+ //----------------//
+ /**
+ * Creates a new FolderSelector object.
+ */
+ public FolderSelector (Bundle bundle)
+ {
+ this.bundle = bundle;
+
+ component = defineLayout();
+
+ ///////path.setText(descriptor.getInstallFolder().getAbsolutePath());
+ path.addActionListener(this);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------------//
+ // actionPerformed //
+ //-----------------//
+ /**
+ * Triggered from path JTextField.
+ *
+ * @param e the action event
+ */
+ @Override
+ public void actionPerformed (ActionEvent e)
+ {
+ logger.debug("Got event {}", e);
+ checkFolder(new File(path.getText()));
+ }
+
+ //--------------//
+ // getComponent //
+ //--------------//
+ /**
+ * @return the component
+ */
+ public JComponent getComponent ()
+ {
+ return component;
+ }
+
+ //-------------//
+ // checkFolder //
+ //-------------//
+ /**
+ * Make sure the provided candidate is OK
+ *
+ * @param file the candidate folder
+ */
+ private void checkFolder (File candidate)
+ {
+ try {
+ if (!candidate.exists()) {
+ // Make sure all directories are created
+ if (candidate.mkdirs()) {
+ logger.info("Created folder {}", candidate.getAbsolutePath());
+ }
+ } else {
+ if (!candidate.isDirectory()) {
+ throw new IllegalStateException("Not a directory");
+ }
+ }
+
+ // All tests are OK
+ path.setText(candidate.getAbsolutePath());
+ ////////bundle.setInstallFolder(candidate);
+ } catch (Exception ex) {
+ if (Installer.hasUI()) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ "Invalid folder: " + ex,
+ "Folder selection",
+ JOptionPane.WARNING_MESSAGE);
+ }
+ }
+ }
+
+ //--------------//
+ // defineLayout //
+ private JComponent defineLayout ()
+ {
+ final JPanel comp = new JPanel();
+ final FormLayout layout = new FormLayout(
+ "right:36dlu, $lcgap, fill:0:grow, $lcgap, 31dlu",
+ "pref");
+ final CellConstraints cst = new CellConstraints();
+ final PanelBuilder builder = new PanelBuilder(layout, comp);
+
+ // Label on left side
+ builder.addROLabel("Folder", cst.xy(1, 1));
+
+ // Path to folder
+ builder.add(path, cst.xy(3, 1));
+
+ // "Select" button on left side
+ builder.add(new JButton(new BrowseAction()), cst.xy(5, 1));
+
+ return comp;
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //--------------//
+ // BrowseAction //
+ //--------------//
+ private class BrowseAction
+ extends AbstractAction
+ {
+ //~ Constructors -------------------------------------------------------
+
+ public BrowseAction ()
+ {
+ putValue(AbstractAction.NAME, "Select");
+ putValue(
+ AbstractAction.SHORT_DESCRIPTION,
+ "Select another installation folder");
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public void actionPerformed (ActionEvent e)
+ {
+// // We always launch browsing from default installation folder
+// JFileChooser chooser = new JFileChooser(
+// descriptor.getInstallFolder());
+// chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+//
+// int opt = chooser.showDialog(
+// Installer.getFrame(),
+// "Select install folder");
+//
+// if (opt == JFileChooser.APPROVE_OPTION) {
+// checkFolder(chooser.getSelectedFile());
+// }
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/GhostscriptCompanion.java b/src/installer/com/audiveris/installer/GhostscriptCompanion.java
new file mode 100644
index 0000000..ccc9100
--- /dev/null
+++ b/src/installer/com/audiveris/installer/GhostscriptCompanion.java
@@ -0,0 +1,75 @@
+//----------------------------------------------------------------------------//
+// //
+// G h o s t s c r i p t C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class {@code GhostscriptCompanion} handles the installation of
+ * Ghostscript.
+ *
+ * @author Hervé Bitteur
+ */
+class GhostscriptCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final org.slf4j.Logger logger = LoggerFactory.getLogger(
+ GhostscriptCompanion.class);
+
+ private static final String DESC = "Ghostscript component is mandatory."
+ + " It allows to process PDF input files.";
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ //~ Constructors -----------------------------------------------------------
+ //----------------------//
+ // GhostscriptCompanion //
+ //----------------------//
+ /**
+ * Creates a new GhostscriptCompanion object.
+ */
+ public GhostscriptCompanion ()
+ {
+ super("Ghostscript", DESC);
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 95);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // checkInstalled //
+ //----------------//
+ @Override
+ public boolean checkInstalled ()
+ {
+ status = descriptor.isGhostscriptInstalled() ? Status.INSTALLED
+ : Status.NOT_INSTALLED;
+
+ return status == Status.INSTALLED;
+ }
+
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ descriptor.installGhostscript();
+ }
+}
diff --git a/src/installer/com/audiveris/installer/Installer.java b/src/installer/com/audiveris/installer/Installer.java
new file mode 100644
index 0000000..333c9ef
--- /dev/null
+++ b/src/installer/com/audiveris/installer/Installer.java
@@ -0,0 +1,313 @@
+//----------------------------------------------------------------------------//
+// //
+// I n s t a l l e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+
+import javax.jnlp.UnavailableServiceException;
+import javax.swing.JOptionPane;
+
+/**
+ * Class {@code Installer} is the main class for installation of
+ * Audiveris complete bundle using Java Web Start technology.
+ *
+ * We can assume that, thanks to Java Web Start, a proper JRE version is made
+ * available for this installer before it is launched.
+ * Since subsequent Audiveris application needs java 1.7 as of this writing,
+ * we can base this Installer on Java 1.7 as well.
+ *
+ * @author Hervé Bitteur
+ */
+public class Installer
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ static {
+ /** Configure logging. */
+ LogUtilities.initialize();
+ }
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ Installer.class);
+
+ /** Single instance. */
+ private static Installer INSTANCE;
+
+ /** Flag an installation. */
+ private static boolean isInstall;
+
+ /** Flag an uninstallation. */
+ private static boolean isUninstall;
+
+ /** Flag a process running as administrator. */
+ public static boolean isAdmin;
+
+ /** Flag an interactive (vs batch) run. */
+ private static boolean hasUI;
+
+ /** To force main thread to wait for UI completion. */
+ public static CountDownLatch latch = new CountDownLatch(1);
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** Bundle of companions to install (or insinstall). */
+ private final Bundle bundle;
+
+ //~ Constructors -----------------------------------------------------------
+ //-----------//
+ // Installer //
+ //-----------//
+ /**
+ * Creates a new Installer object.
+ */
+ private Installer ()
+ {
+ Jnlp.extensionInstallerService.setHeading(
+ "Running Audiveris installer.");
+
+ // Handling the bundle of all companions
+ bundle = new Bundle(hasUI);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------//
+ // getBundle //
+ //-----------//
+ /**
+ * Report the bundle of companions to process.
+ *
+ * @return the bundle of companions.
+ */
+ public static Bundle getBundle ()
+ {
+ if (INSTANCE == null) {
+ return null;
+ }
+
+ return INSTANCE.bundle;
+ }
+
+ //----------//
+ // getFrame //
+ //----------//
+ /**
+ * Convenient method to get access to the Installer frame.
+ * Useful for example to locate dialogs with respect to this frame.
+ *
+ * @return the Installer frame
+ */
+ public static BundleView getFrame ()
+ {
+ Bundle bundle = getBundle();
+
+ return (bundle != null) ? bundle.getView() : null;
+ }
+
+ //-------//
+ // hasUI //
+ //-------//
+ /**
+ * Report whether the Installer is run in interactive mode.
+ *
+ * @return true if interactive
+ */
+ public static boolean hasUI ()
+ {
+ return hasUI;
+ }
+
+ //---------//
+ // install //
+ //---------//
+ /**
+ * Launch installation.
+ */
+ public void install ()
+ {
+ logger.debug("install");
+
+ try {
+ // Use a fresh install folder
+ bundle.deleteTempFolder();
+ bundle.createTempFolder();
+
+ // Get all initial installation statuses
+ bundle.checkInstallations();
+
+ ///installerService.hideProgressBar();
+ if (hasUI) {
+ logger.info(
+ "\nEnvironment:\n"
+ + "- OS: {}\n"
+ + "- Architecture: {}\n"
+ + "- Java VM: {}\n",
+ System.getProperty("os.name") + " "
+ + System.getProperty("os.version"),
+ System.getProperty("os.arch"),
+ System.getProperty("java.vm.name") + " (build "
+ + System.getProperty("java.vm.version") + ", "
+ + System.getProperty("java.vm.info") + ")");
+ logger.info(
+ "Please:\n"
+ + "- Add or remove OCR-supported languages,\n"
+ + "- Check or uncheck optional components,\n"
+ + "- Launch installation.\n");
+
+ // Wait until UI has finished...
+ latch.await();
+
+ // To avoid immediate closing...
+ // JOptionPane.showMessageDialog(
+ // Installer.getFrame(),
+ // "Closing time!",
+ // "This is the end",
+ // JOptionPane.INFORMATION_MESSAGE);
+
+ // What if the user has simply not launched the install?
+ if (bundle.isCancelled()) {
+ Jnlp.extensionInstallerService.installFailed();
+ }
+ } else {
+ bundle.installBundle();
+
+ Jnlp.extensionInstallerService.installSucceeded(false);
+ }
+ } catch (Throwable ex) {
+ logger.error("Error encountered", ex);
+
+ if (hasUI) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ "Installation has failed",
+ "Installation completion",
+ JOptionPane.WARNING_MESSAGE);
+ }
+
+ Jnlp.extensionInstallerService.installFailed();
+ } finally {
+ bundle.close();
+ }
+ }
+
+ //------//
+ // main //
+ //------//
+ /**
+ * The Installer.main method is called by Java Web Start on two
+ * occasions within the application cycle: at installation time
+ * (with single argument {@code install}) and at de-installation
+ * time (with single argument {@code uninstall}).
+ *
+ * @param args either "install" or "uninstall"
+ * @throws UnavailableServiceException
+ */
+ public static void main (String[] args)
+ throws UnavailableServiceException
+ {
+ if (args.length == 0) {
+ throw new IllegalArgumentException(
+ "No argument for Installer."
+ + " Expecting install or uninstall.");
+ }
+
+ // Interactive or batch mode?
+ String mode = System.getProperty("installer-mode", "interactive");
+ hasUI = !mode.trim()
+ .equalsIgnoreCase("batch");
+ logger.info("Mode {}.", hasUI ? "interactive" : "batch");
+
+ Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ isInstall = isArgSet("install", args);
+ isUninstall = isArgSet("uninstall", args);
+ isAdmin = descriptor.isAdmin();
+
+ if (isAdmin) {
+ logger.info("Running as administrator.");
+ }
+
+ INSTANCE = new Installer();
+
+ if (isInstall) {
+ logger.info("Performing install.");
+ INSTANCE.install();
+ } else if (isUninstall) {
+ logger.info("Performing uninstall.");
+ INSTANCE.uninstall();
+ } else {
+ // Not a valid argument array
+ logger.error(
+ "Illegal Installer arguments: {}",
+ Arrays.deepToString(args));
+ }
+ }
+
+ //-----------//
+ // uninstall //
+ //-----------//
+ /**
+ * Launch uninstallation.
+ */
+ public void uninstall ()
+ {
+ logger.debug("uninstall");
+
+ try {
+ bundle.uninstallBundle();
+
+ if (hasUI) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ "Bundle successfully uninstalled",
+ "Uninstallation completion",
+ JOptionPane.INFORMATION_MESSAGE);
+ }
+ } catch (Throwable ex) {
+ if (hasUI) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ "Uninstallation has failed: " + ex,
+ "Uninstallation completion",
+ JOptionPane.WARNING_MESSAGE);
+ }
+ } finally {
+ //bundle.close();
+ }
+ }
+
+ //----------//
+ // isArgSet //
+ //----------//
+ /**
+ * Check whether the provided name appears in the args array.
+ *
+ * @param name the name of interest
+ * @param args the program arguments
+ * @return true if found, false otherwise
+ */
+ private static boolean isArgSet (String name,
+ String[] args)
+ {
+ for (String arg : args) {
+ if (arg.equalsIgnoreCase(name)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/JarExpander.java b/src/installer/com/audiveris/installer/JarExpander.java
new file mode 100644
index 0000000..83fcc28
--- /dev/null
+++ b/src/installer/com/audiveris/installer/JarExpander.java
@@ -0,0 +1,226 @@
+//----------------------------------------------------------------------------//
+// //
+// J a r E x p a n d e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+/**
+ * Class {@code JarExpander} handles the installation of items from a
+ * .jar archive.
+ * This is targeted to complement the use of .jar archive for which some
+ * directories need to be "browsable" and thus expanded as directories and files
+ * to a given location prior to their browsing.
+ *
+ * @author Hervé Bitteur
+ */
+public class JarExpander
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ JarExpander.class);
+
+ //~ Instance fields --------------------------------------------------------
+ /** Jar file. */
+ private final JarFile jar;
+
+ /** Source folder. */
+ private final String sourceFolder;
+
+ /** Precise target folder. */
+ private final File targetFolder;
+
+ //~ Constructors -----------------------------------------------------------
+ //-------------//
+ // JarExpander //
+ //-------------//
+ /**
+ * Create an JarExpander object for a given folder.
+ *
+ * @param jar the jar file to extract data from
+ * @param sourceFolder name of source folder
+ * @param targetFolder precise target folder
+ */
+ public JarExpander (final JarFile jar,
+ final String sourceFolder,
+ final File targetFolder)
+ {
+ this.jar = jar;
+ this.sourceFolder = sourceFolder;
+ this.targetFolder = targetFolder;
+
+ logger.debug(
+ "From jar {} expanding {} entries to folder {}",
+ jar.getName(),
+ sourceFolder,
+ targetFolder);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //---------//
+ // install //
+ //---------//
+ /**
+ * Install, if needed, the desired folder.
+ *
+ * @return the number of entries actually written
+ */
+ public int install ()
+ {
+ // Retrieve source entries in jar file
+ List sources = getSourceEntries();
+
+ if (sources.isEmpty()) {
+ logger.warn("No sources for folder {}", sourceFolder);
+
+ return 0;
+ }
+
+ // Compare file by file, and update if necessary
+ int copied = 0; // Number of entries copied
+
+ for (String source : sources) {
+ copied += process(source);
+ }
+
+ if (copied != 0) {
+ logger.info("{} entries copied to {}", copied, targetFolder);
+ }
+
+ return copied;
+ }
+
+ //------------------//
+ // getSourceEntries //
+ //------------------//
+ /**
+ * Retrieve all entries from the jar file that match the folder
+ * to copy
+ *
+ * @return the list of entries found, perhaps empty but not null
+ */
+ private List getSourceEntries ()
+ {
+ final List found = new ArrayList();
+ final Enumeration entries = jar.entries();
+ final String prefix = sourceFolder.isEmpty() ? ""
+ : (sourceFolder.endsWith("/")
+ ? sourceFolder
+ : (sourceFolder + "/"));
+ logger.debug("Prefix is '{}'", prefix);
+
+ // If sourceFolder == "" we take all rooted files,
+ // excluding the manifest stuff: META-INF/...
+ while (entries.hasMoreElements()) {
+ final String entry = entries.nextElement()
+ .getName();
+
+ if (prefix.isEmpty()) {
+ // Just skip the meta inf stuff
+ if (entry.startsWith("META-INF")) {
+ logger.trace("Skipping meta-inf {}", entry);
+ } else {
+ logger.trace("Found {}", entry);
+ found.add(entry);
+ }
+ } else if (entry.startsWith(prefix)) {
+ logger.trace("Found {}", entry);
+ found.add(entry);
+ } else {
+ logger.trace("Skipping {}", entry);
+ }
+ }
+
+ return found;
+ }
+
+ //---------//
+ // process //
+ //---------//
+ /**
+ * Process one entry.
+ *
+ * @param source the source entry to check and install
+ * @return 1 if actually copied, 0 otherwise
+ */
+ private int process (String source)
+ {
+ ///logger.debug("Processing source {}", source);
+ final String sourcePath = sourceFolder.isEmpty() ? source
+ : source.substring(sourceFolder.length() + 1);
+ final Path target = Paths.get(targetFolder.toString(), sourcePath);
+
+ try {
+ if (source.endsWith("/")) {
+ // This is a directory
+ if (Files.exists(target)) {
+ if (!Files.isDirectory(target)) {
+ logger.warn("Existing non directory {}", target);
+ } else {
+ logger.trace("Directory {} exists", target);
+ }
+
+ return 0;
+ } else {
+ Files.createDirectories(target);
+ logger.trace("Created dir {}", target);
+
+ return 1;
+ }
+ } else {
+ ZipEntry entry = jar.getEntry(source);
+
+ // Target exists?
+ if (Files.exists(target)) {
+ // Compare date
+ FileTime sourceTime = FileTime.fromMillis(entry.getTime());
+ FileTime targetTime = Files.getLastModifiedTime(target);
+
+ if (targetTime.compareTo(sourceTime) >= 0) {
+ logger.trace("Target {} is up to date", target);
+
+ return 0;
+ }
+ }
+
+ // Do copy
+ ///logger.info("About to copy {} to {}", source, target);
+ try (InputStream is = jar.getInputStream(entry)) {
+ Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
+ logger.trace("Target {} copied", target);
+ return 1;
+ }
+ }
+ } catch (IOException ex) {
+ logger.warn("IOException on " + target, ex);
+
+ return 0;
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/Jnlp.java b/src/installer/com/audiveris/installer/Jnlp.java
new file mode 100644
index 0000000..0f70a18
--- /dev/null
+++ b/src/installer/com/audiveris/installer/Jnlp.java
@@ -0,0 +1,598 @@
+//----------------------------------------------------------------------------//
+// //
+// J n l p //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jnlp.BasicService;
+import javax.jnlp.DownloadService;
+import javax.jnlp.DownloadServiceListener;
+import javax.jnlp.ExtensionInstallerService;
+import javax.jnlp.ServiceManager;
+import javax.jnlp.UnavailableServiceException;
+
+/**
+ * Class {@code Jnlp} is a facade to JNLP services to allow
+ * running with and without real JNLP services.
+ *
+ * @author Hervé Bitteur
+ */
+public class Jnlp
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Jnlp.class);
+
+ /** Extension installer service. */
+ public static final ExtensionInstallerService extensionInstallerService = new ExtensionInstallerServiceFacade();
+
+ /** Basic service. */
+ public static final BasicService basicService = new BasicServiceFacade();
+
+ /** Download service. */
+ public static final DownloadService downloadService = new DownloadServiceFacade();
+
+ //~ Inner Classes ----------------------------------------------------------
+
+ //--------------------//
+ // BasicServiceFacade //
+ //--------------------//
+ private static class BasicServiceFacade
+ implements BasicService
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private BasicService service;
+
+ //~ Constructors -------------------------------------------------------
+
+ public BasicServiceFacade ()
+ {
+ try {
+ service = (BasicService) ServiceManager.lookup(
+ "javax.jnlp.BasicService");
+ logger.debug("Real BasicService available");
+
+ // Use JNLP cache for all downloads
+ JnlpResponseCache.init();
+ } catch (UnavailableServiceException ex) {
+ logger.debug("No BasicService available");
+ }
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public URL getCodeBase ()
+ {
+ URL res = null;
+
+ if (service != null) {
+ res = service.getCodeBase();
+ } else {
+ try {
+ res = new File(".").toURI()
+ .toURL();
+ } catch (MalformedURLException ex) {
+ logger.error("URL error", ex);
+ }
+ }
+
+ logger.debug("getCodeBase => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isOffline ()
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isOffline();
+ }
+
+ logger.debug("isOffline => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isWebBrowserSupported ()
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isWebBrowserSupported();
+ }
+
+ logger.debug("isWebBrowserSupported => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public boolean showDocument (URL url)
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.showDocument(url);
+ }
+
+ logger.debug("showDocument url: {}, => {}", url, res);
+
+ return res;
+ }
+ }
+
+ //-----------------------//
+ // DownloadServiceFacade //
+ //-----------------------//
+ private static class DownloadServiceFacade
+ implements DownloadService
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private DownloadService service;
+
+ //~ Constructors -------------------------------------------------------
+
+ public DownloadServiceFacade ()
+ {
+ try {
+ service = (DownloadService) ServiceManager.lookup(
+ "javax.jnlp.DownloadService");
+ logger.debug("Real DownloadService available");
+ } catch (UnavailableServiceException ex) {
+ logger.debug("No DownloadService available");
+ }
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public DownloadServiceListener getDefaultProgressWindow ()
+ {
+ DownloadServiceListener res = null;
+
+ if (service != null) {
+ res = service.getDefaultProgressWindow();
+ }
+
+ logger.debug("getDefaultProgressWindow => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isExtensionPartCached (URL url,
+ String string,
+ String string1)
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isExtensionPartCached(url, string, string1);
+ }
+
+ logger.debug(
+ "isExtensionPartCached url: {}, string: {}, string1: {} => {}",
+ url,
+ string,
+ string1,
+ res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isExtensionPartCached (URL url,
+ String string,
+ String[] strings)
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isExtensionPartCached(url, string, strings);
+ }
+
+ logger.debug(
+ "isExtensionPartCached url: {}, string: {}, strings: {} => {}",
+ url,
+ string,
+ strings,
+ res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isPartCached (String string)
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isPartCached(string);
+ }
+
+ logger.debug("isPartCached string: {} => {}", string, res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isPartCached (String[] strings)
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isPartCached(strings);
+ }
+
+ logger.debug("isPartCached strings: {} => {}", strings, res);
+
+ return res;
+ }
+
+ @Override
+ public boolean isResourceCached (URL url,
+ String string)
+ {
+ boolean res = false;
+
+ if (service != null) {
+ res = service.isResourceCached(url, string);
+ }
+
+ logger.debug(
+ "isResourceCached url: {}, string: {} => {}",
+ url,
+ string,
+ res);
+
+ return res;
+ }
+
+ @Override
+ public void loadExtensionPart (URL url,
+ String string,
+ String string1,
+ DownloadServiceListener dl)
+ throws IOException
+ {
+ logger.debug(
+ "loadExtensionPart url: {}, string: {}, string1: {}, dl: {}",
+ url,
+ string,
+ string1,
+ dl);
+
+ if (service != null) {
+ service.loadExtensionPart(url, string, string1, dl);
+ }
+ }
+
+ @Override
+ public void loadExtensionPart (URL url,
+ String string,
+ String[] strings,
+ DownloadServiceListener dl)
+ throws IOException
+ {
+ logger.debug(
+ "loadExtensionPart url: {}, string: {}, strings: {}, dl: {}",
+ url,
+ string,
+ strings,
+ dl);
+
+ if (service != null) {
+ service.loadExtensionPart(url, string, strings, dl);
+ }
+ }
+
+ @Override
+ public void loadPart (String string,
+ DownloadServiceListener dl)
+ throws IOException
+ {
+ logger.debug("loadPart string: {}, dl: {}", string, dl);
+
+ if (service != null) {
+ service.loadPart(string, dl);
+ }
+ }
+
+ @Override
+ public void loadPart (String[] strings,
+ DownloadServiceListener dl)
+ throws IOException
+ {
+ logger.debug("loadPart strings: {}, dl: {}", strings, dl);
+
+ if (service != null) {
+ service.loadPart(strings, dl);
+ }
+ }
+
+ @Override
+ public void loadResource (URL url,
+ String string,
+ DownloadServiceListener dl)
+ throws IOException
+ {
+ logger.debug(
+ "loadResource url: {}, string: {}, dl: {}",
+ url,
+ string,
+ dl);
+
+ if (service != null) {
+ service.loadResource(url, string, dl);
+ }
+ }
+
+ @Override
+ public void removeExtensionPart (URL url,
+ String string,
+ String string1)
+ throws IOException
+ {
+ logger.debug(
+ "removeExtensionPart url: {}, string: {}, string1: {}",
+ url,
+ string,
+ string1);
+
+ if (service != null) {
+ service.removeExtensionPart(url, string, string1);
+ }
+ }
+
+ @Override
+ public void removeExtensionPart (URL url,
+ String string,
+ String[] strings)
+ throws IOException
+ {
+ logger.debug(
+ "removeExtensionPart url: {}, string: {}, strings: {}",
+ url,
+ string,
+ strings);
+
+ if (service != null) {
+ service.removeExtensionPart(url, string, strings);
+ }
+ }
+
+ @Override
+ public void removePart (String string)
+ throws IOException
+ {
+ logger.debug("removePart string: {}", string);
+
+ if (service != null) {
+ service.removePart(string);
+ }
+ }
+
+ @Override
+ public void removePart (String[] strings)
+ throws IOException
+ {
+ logger.debug("removePart strings: {}", strings.toString());
+
+ if (service != null) {
+ service.removePart(strings);
+ }
+ }
+
+ @Override
+ public void removeResource (URL url,
+ String string)
+ throws IOException
+ {
+ logger.debug("removeResource url: {}, string: {}", url, string);
+
+ if (service != null) {
+ service.removeResource(url, string);
+ }
+ }
+ }
+
+ //---------------------------------//
+ // ExtensionInstallerServiceFacade //
+ //---------------------------------//
+ private static class ExtensionInstallerServiceFacade
+ implements ExtensionInstallerService
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** Real service, if any. */
+ private ExtensionInstallerService service;
+
+ //~ Constructors -------------------------------------------------------
+
+ public ExtensionInstallerServiceFacade ()
+ {
+ try {
+ service = (ExtensionInstallerService) ServiceManager.lookup(
+ "javax.jnlp.ExtensionInstallerService");
+ logger.debug("Real ExtensionInstallerService available");
+
+ logger.debug("ExtensionLocation = {}", getExtensionLocation());
+ logger.debug("InstallPath = {}", getInstallPath());
+ } catch (UnavailableServiceException ex) {
+ logger.debug("No ExtensionInstallerService available");
+ }
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public URL getExtensionLocation ()
+ {
+ URL res = null;
+
+ if (service != null) {
+ res = service.getExtensionLocation();
+ }
+
+ logger.debug("getExtensionLocation => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public String getExtensionVersion ()
+ {
+ String res = null;
+
+ if (service != null) {
+ res = service.getExtensionVersion();
+ }
+
+ logger.debug("getExtensionVersion => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public String getInstallPath ()
+ {
+ String res = null;
+
+ if (service != null) {
+ res = service.getInstallPath();
+ }
+
+ logger.debug("getInstallPath => {}", res);
+
+ return res;
+ }
+
+ @Override
+ public String getInstalledJRE (URL url,
+ String string)
+ {
+ logger.debug("getInstalledJRE url: {} string: {}", url, string);
+
+ String res = null;
+
+ if (service != null) {
+ res = service.getInstalledJRE(url, string);
+ }
+
+ return res;
+ }
+
+ @Override
+ public void hideProgressBar ()
+ {
+ logger.debug("hideProgressBar");
+
+ if (service != null) {
+ service.hideProgressBar();
+ }
+ }
+
+ @Override
+ public void hideStatusWindow ()
+ {
+ logger.debug("hideStatusWindow");
+
+ if (service != null) {
+ service.hideStatusWindow();
+ }
+ }
+
+ @Override
+ public void installFailed ()
+ {
+ logger.debug("installFailed");
+
+ if (service != null) {
+ service.installFailed();
+ }
+ }
+
+ @Override
+ public void installSucceeded (boolean reboot)
+ {
+ logger.debug("installSucceeded reboot: {}", reboot);
+
+ if (service != null) {
+ service.installSucceeded(reboot);
+ }
+ }
+
+ @Override
+ public void setHeading (String string)
+ {
+ logger.debug("setHeading string: {}", string);
+
+ if (service != null) {
+ service.setHeading(string);
+ }
+ }
+
+ @Override
+ public void setJREInfo (String string,
+ String string1)
+ {
+ logger.debug("setJREInfo string: {} string1: {}", string, string1);
+
+ if (service != null) {
+ service.setJREInfo(string, string1);
+ }
+ }
+
+ @Override
+ public void setNativeLibraryInfo (String string)
+ {
+ logger.debug("setNativeLibraryInfo string: {}", string);
+
+ if (service != null) {
+ service.setNativeLibraryInfo(string);
+ }
+ }
+
+ @Override
+ public void setStatus (String string)
+ {
+ logger.debug("setStatus string: {}", string);
+
+ if (service != null) {
+ service.setStatus(string);
+ }
+ }
+
+ @Override
+ public void updateProgress (int i)
+ {
+ logger.debug("updateProgress i: {}", i);
+
+ if (service != null) {
+ service.updateProgress(i);
+ }
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/JnlpResponseCache.java b/src/installer/com/audiveris/installer/JnlpResponseCache.java
new file mode 100644
index 0000000..c391c47
--- /dev/null
+++ b/src/installer/com/audiveris/installer/JnlpResponseCache.java
@@ -0,0 +1,93 @@
+//----------------------------------------------------------------------------//
+// //
+// J n l p R e s p o n s e C a c h e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+import javax.jnlp.DownloadService;
+import javax.jnlp.ServiceManager;
+import javax.jnlp.UnavailableServiceException;
+
+/**
+ * Class {@code JnlpResponseCache} enables the Java Cache for JNLP.
+ * You need to call once JnlpResponseCache.init() in your code to enable it.
+ *
+ * @author horcrux7 http://stackoverflow.com/users/12631/horcrux7
+ */
+public class JnlpResponseCache
+ extends ResponseCache
+{
+ //~ Instance fields --------------------------------------------------------
+
+ private final DownloadService service;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //-------------------//
+ // JnlpResponseCache //
+ //-------------------//
+ private JnlpResponseCache ()
+ {
+ try {
+ service = (DownloadService) ServiceManager.lookup(
+ "javax.jnlp.DownloadService");
+ } catch (UnavailableServiceException ex) {
+ throw new NoClassDefFoundError(ex.toString());
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //---------------//
+ // CacheResponse //
+ //---------------//
+ @Override
+ public CacheResponse get (URI uri,
+ String rqstMethod,
+ Map> rqstHeaders)
+ throws IOException
+ {
+ return null;
+ }
+
+ //--------------//
+ // CacheRequest //
+ //--------------//
+ @Override
+ public CacheRequest put (URI uri,
+ URLConnection conn)
+ throws IOException
+ {
+ URL url = uri.toURL();
+ service.loadResource(url, null, service.getDefaultProgressWindow());
+
+ return null;
+ }
+
+ //------//
+ // init //
+ //------//
+ static void init ()
+ {
+ if (ResponseCache.getDefault() == null) {
+ ResponseCache.setDefault(new JnlpResponseCache());
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/LangSelector.java b/src/installer/com/audiveris/installer/LangSelector.java
new file mode 100644
index 0000000..990c534
--- /dev/null
+++ b/src/installer/com/audiveris/installer/LangSelector.java
@@ -0,0 +1,400 @@
+//----------------------------------------------------------------------------//
+// //
+// L a n g S e l e c t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import com.jgoodies.forms.builder.PanelBuilder;
+import com.jgoodies.forms.factories.CC;
+import com.jgoodies.forms.layout.CellConstraints;
+import com.jgoodies.forms.layout.FormLayout;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JList;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+
+/**
+ * Class {@code LangSelector} handles the collection of OCR languages,
+ * displays a banner of relevant languages, with the ability
+ * to add or remove languages.
+ *
+ * If no language is desired, bundle installation cannot be launched.
+ *
+ * @author Hervé Bitteur
+ */
+public class LangSelector
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ LangSelector.class);
+
+ //~ Instance fields --------------------------------------------------------
+
+ /** Related companion. */
+ private final OcrCompanion companion;
+
+ /** Component. */
+ private JPanel component;
+
+ /** Display of handled languages. */
+ private Banner banner;
+
+ /** Desired languages. */
+ private Set desired;
+
+ /** Non-desired languages. */
+ private Set nonDesired;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //--------------//
+ // LangSelector //
+ //--------------//
+ /**
+ * Creates a new LangSelector object.
+ *
+ * @param companion the language companion
+ */
+ public LangSelector (OcrCompanion companion)
+ {
+ this.companion = companion;
+
+ desired = companion.getDesired();
+ nonDesired = companion.getNonDesired();
+
+ component = defineLayout();
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //--------------//
+ // getComponent //
+ //--------------//
+ public JPanel getComponent ()
+ {
+ return component;
+ }
+
+ //--------//
+ // update //
+ //--------//
+ /**
+ * Update the banner display.
+ */
+ public void update (final String currentLang)
+ {
+ // It's easier to merely rebuild the banner component
+ banner.defineLayout(currentLang);
+ }
+
+ //--------------//
+ // defineLayout //
+ //--------------//
+ private JPanel defineLayout ()
+ {
+ final JPanel comp = new JPanel();
+ final FormLayout layout = new FormLayout(
+ "right:40dlu, $lcgap, fill:0:grow, $lcgap, 33dlu",
+ "pref");
+ final CellConstraints cst = new CellConstraints();
+ final PanelBuilder builder = new PanelBuilder(layout, comp);
+
+ // Label on left side
+ builder.addROLabel("Languages", cst.xy(1, 1));
+
+ // "Banner" for the center of the line
+ banner = new Banner();
+ builder.add(banner.getComponent(), cst.xy(3, 1));
+
+ // "Add" button on right side
+ JButton button = new JButton(new AddAction());
+ builder.add(button, cst.xy(5, 1));
+
+ return comp;
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+
+ //-----------//
+ // AddAction //
+ //-----------//
+ /** Addition of one or several languages. */
+ private class AddAction
+ extends AbstractAction
+ {
+ //~ Constructors -------------------------------------------------------
+
+ public AddAction ()
+ {
+ putValue(AbstractAction.NAME, "Add");
+ putValue(
+ AbstractAction.SHORT_DESCRIPTION,
+ "Add one or several languages");
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public void actionPerformed (ActionEvent e)
+ {
+ // Create a dialog with a JList of possible additions
+ // That is all languages, minus those already desired
+ List additionals = new ArrayList(
+ Arrays.asList(OcrCompanion.ALL_LANGUAGES));
+ additionals.removeAll(desired);
+
+ JList list = new JList(
+ additionals.toArray(new String[additionals.size()]));
+ JScrollPane scrollPane = new JScrollPane(list);
+ list.setLayoutOrientation(JList.VERTICAL_WRAP);
+ list.setVisibleRowCount(10);
+
+ // Let the user select additional languages
+ int opt = JOptionPane.showConfirmDialog(
+ Installer.getFrame(),
+ scrollPane,
+ "OCR languages selection",
+ JOptionPane.OK_CANCEL_OPTION,
+ JOptionPane.QUESTION_MESSAGE);
+ List toAdd = list.getSelectedValuesList();
+ logger.debug("Opt: {} Selection: {}", opt, toAdd);
+
+ // Save the selection, only if OK
+ if (opt == JOptionPane.OK_OPTION) {
+ logger.info("Additional languages: {}", toAdd);
+ desired.addAll(toAdd);
+
+ // This may impact the "installed" status of the companion
+ companion.checkInstalled();
+ banner.defineLayout(null);
+ companion.updateView();
+ }
+ }
+ }
+
+ //--------//
+ // Banner //
+ //--------//
+ /**
+ * The banner displays all relevant languages with their status.
+ * desired / installed : green, nothing to do
+ * desired / not installed : pink, installation scheduled
+ * not desired / installed : gray, removal scheduled
+ * not desired / not installed : not relevant, nothing to do
+ */
+ private class Banner
+ implements ActionListener
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private final JPanel panel = new JPanel();
+ private final JScrollPane scrollPane = new JScrollPane(panel);
+
+ //~ Constructors -------------------------------------------------------
+
+ public Banner ()
+ {
+ panel.setBorder(null);
+ scrollPane.setBorder(null);
+ scrollPane.setVerticalScrollBarPolicy(
+ JScrollPane.VERTICAL_SCROLLBAR_NEVER);
+
+ defineLayout(null);
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ /** Triggered by popup selection. */
+ @Override
+ public void actionPerformed (ActionEvent e)
+ {
+ // Remove the designated language
+ final JMenuItem item = (JMenuItem) e.getSource();
+ final String lang = item.getName();
+ logger.debug("del lang: {}", lang);
+
+ desired.remove(lang);
+
+ if (companion.isLangInstalled(lang)) {
+ nonDesired.add(lang);
+ logger.info("Language {} to be removed", lang);
+ }
+
+ // This may impact the "installed" status of the companion
+ companion.checkInstalled();
+ banner.defineLayout(null);
+ companion.updateView();
+ }
+
+ public final void defineLayout (final String currentLang)
+ {
+ panel.removeAll();
+
+ final Set relevant = new TreeSet();
+ relevant.addAll(desired);
+ relevant.addAll(nonDesired);
+
+ final String gap = "$lcgap";
+ final StringBuilder columns = new StringBuilder();
+
+ for (int i = 0; i < relevant.size(); i++) {
+ if (columns.length() > 0) {
+ columns.append(",");
+ }
+
+ columns.append(gap)
+ .append(",")
+ .append("pref");
+ }
+
+ final CellConstraints cst = new CellConstraints();
+ final FormLayout layout = new FormLayout(
+ columns.toString(),
+ "center:16dlu");
+ final PanelBuilder builder = new PanelBuilder(layout, panel);
+ PanelBuilder.setOpaqueDefault(true);
+ builder.background(Color.WHITE);
+
+ int col = 2;
+
+ for (String lang : relevant) {
+ final FormLayout langLayout = new FormLayout(
+ "$lcgap,center:pref,$lcgap",
+ "center:12dlu");
+ final JPanel comp = new JPanel();
+ comp.setBackground(getBackground(lang, currentLang));
+
+ final PanelBuilder langBuilder = new PanelBuilder(
+ langLayout,
+ comp);
+ langBuilder.addROLabel(lang, CC.xy(2, 1));
+ comp.addMouseListener(createPopupListener(lang));
+ comp.setToolTipText("Use right-click to remove this language");
+
+ builder.add(comp, cst.xy(col, 1));
+ col += 2;
+ }
+
+ panel.revalidate();
+ panel.repaint();
+ }
+
+ public JComponent getComponent ()
+ {
+ return scrollPane;
+ }
+
+ protected Color getBackground (final String language,
+ final String current)
+ {
+ if (language.equals(current)) {
+ return CompanionView.COLORS.BEING;
+ }
+
+ if (companion.isLangInstalled(language)) {
+ if (desired.contains(language)) {
+ return CompanionView.COLORS.INST;
+ } else {
+ return CompanionView.COLORS.UNUSED;
+ }
+ } else {
+ return CompanionView.COLORS.NOT_INST;
+ }
+ }
+
+ private MouseListener createPopupListener (String lang)
+ {
+ // Create a very simple popup menu.
+ final JPopupMenu popup = new JPopupMenu();
+ final JMenuItem menuItem = new JMenuItem("Remove " + lang);
+ menuItem.setName(lang);
+ menuItem.addActionListener(this);
+ popup.add(menuItem);
+
+ return new PopupListener(popup);
+ }
+ }
+
+ //---------------//
+ // PopupListener //
+ //---------------//
+ private class PopupListener
+ implements MouseListener
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ JPopupMenu popup;
+
+ //~ Constructors -------------------------------------------------------
+
+ PopupListener (JPopupMenu popupMenu)
+ {
+ popup = popupMenu;
+ }
+
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public void mouseClicked (MouseEvent e)
+ {
+ }
+
+ @Override
+ public void mouseEntered (MouseEvent e)
+ {
+ }
+
+ @Override
+ public void mouseExited (MouseEvent e)
+ {
+ }
+
+ @Override
+ public void mousePressed (MouseEvent e)
+ {
+ maybeShowPopup(e);
+ }
+
+ @Override
+ public void mouseReleased (MouseEvent e)
+ {
+ maybeShowPopup(e);
+ }
+
+ private void maybeShowPopup (MouseEvent e)
+ {
+ if (e.isPopupTrigger()) {
+ popup.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/LicenseCompanion.java b/src/installer/com/audiveris/installer/LicenseCompanion.java
new file mode 100644
index 0000000..0bdc498
--- /dev/null
+++ b/src/installer/com/audiveris/installer/LicenseCompanion.java
@@ -0,0 +1,187 @@
+//----------------------------------------------------------------------------//
+// //
+// L i c e n s e C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+
+/**
+ * Class {@code LicenseCompanion} checks user agreement WRT license.
+ *
+ * @author Hervé Bitteur
+ */
+public class LicenseCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ LicenseCompanion.class);
+
+ private static final String DESC = "This ensures you agree with Audiveris license."
+ + " (You will be able to browse the license text)";
+
+ /** Name for license. */
+ private static final String LICENSE_NAME = "GNU GPL V2";
+
+ /** URL for license browsing. */
+ private static final String LICENSE_URL = "http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html";
+
+ //~ Constructors -----------------------------------------------------------
+ //------------------//
+ // LicenseCompanion //
+ //------------------//
+ /**
+ * Creates a new LicenseCompanion object.
+ */
+ public LicenseCompanion ()
+ {
+ super("License", DESC);
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 70);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // checkInstalled //
+ //----------------//
+ @Override
+ public boolean checkInstalled ()
+ {
+ return status == Status.INSTALLED;
+ }
+
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ // When running without UI, we assume license is accepted
+ if (!Installer.hasUI()) {
+ return;
+ }
+
+ // User choice (must be an output, yet final)
+ final boolean[] isOk = new boolean[1];
+
+ final String yes = "Yes";
+ final String no = "No";
+ final String browse = "View License";
+ final JOptionPane optionPane = new JOptionPane(
+ "Do you agree to license " + LICENSE_NAME + "?",
+ JOptionPane.QUESTION_MESSAGE,
+ JOptionPane.YES_NO_CANCEL_OPTION,
+ null,
+ new Object[]{yes, no, browse},
+ yes);
+ final String frameTitle = "End User License Agreement";
+ final JDialog dialog = new JDialog(
+ Installer.getFrame(),
+ frameTitle,
+ true);
+ dialog.setContentPane(optionPane);
+
+ // Prevent dialog closing
+ dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+
+ optionPane.addPropertyChangeListener(
+ new PropertyChangeListener()
+ {
+ @Override
+ public void propertyChange (PropertyChangeEvent e)
+ {
+ String prop = e.getPropertyName();
+
+ if (dialog.isVisible()
+ && (e.getSource() == optionPane)
+ && (prop.equals(JOptionPane.VALUE_PROPERTY))) {
+ Object option = optionPane.getValue();
+ logger.debug("option: {}", option);
+
+ if (option == yes) {
+ isOk[0] = true;
+ dialog.setVisible(false);
+ dialog.dispose();
+ } else if (option == no) {
+ isOk[0] = false;
+ dialog.setVisible(false);
+ dialog.dispose();
+ } else if (option == browse) {
+ logger.info(
+ "Launching browser on {}",
+ LICENSE_URL);
+ showLicense();
+ optionPane.setValue(
+ JOptionPane.UNINITIALIZED_VALUE);
+ } else {
+ }
+ }
+ }
+ });
+
+ dialog.pack();
+ dialog.setLocationRelativeTo(Installer.getFrame());
+ dialog.setVisible(true);
+
+ logger.debug("OK: {}", isOk[0]);
+
+ if (!isOk[0]) {
+ throw new LicenseDeclinedException();
+ }
+ }
+
+ //-------------//
+ // showLicense //
+ //-------------//
+ private void showLicense ()
+ {
+ try {
+ final URL url = new URL(LICENSE_URL);
+
+ if (Jnlp.basicService.isWebBrowserSupported()) {
+ Jnlp.basicService.showDocument(url);
+ } else {
+ logger.warn("Browser is not supported");
+ }
+ } catch (MalformedURLException ex) {
+ logger.error("Malformed URL " + LICENSE_URL, ex);
+ }
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //--------------------------//
+ // LicenseDeclinedException //
+ //--------------------------//
+ public static class LicenseDeclinedException
+ extends RuntimeException
+ {
+ //~ Constructors -------------------------------------------------------
+
+ public LicenseDeclinedException ()
+ {
+ super("License declined by user");
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/LogUtilities.java b/src/installer/com/audiveris/installer/LogUtilities.java
new file mode 100644
index 0000000..ec03a8d
--- /dev/null
+++ b/src/installer/com/audiveris/installer/LogUtilities.java
@@ -0,0 +1,161 @@
+//----------------------------------------------------------------------------//
+// //
+// L o g U t i l i t i e s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.filter.Filter;
+import ch.qos.logback.core.spi.FilterReply;
+import ch.qos.logback.core.util.StatusPrinter;
+
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Class {@code LogUtilities} handles default logging configuration
+ * for the Installer.
+ *
+ * @author Hervé Bitteur
+ */
+public class LogUtilities
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** System property for LogBack configuration. */
+ private static final String LOGBACK_LOGGING_KEY = "logback.configurationFile";
+
+ //~ Methods ----------------------------------------------------------------
+ //------------//
+ // initialize //
+ //------------//
+ /**
+ * Check for (BackLog) logging configuration, and if not found,
+ * define a minimal configuration.
+ * This method should be called at the very beginning of the program before
+ * any logging request is sent.
+ */
+ public static void initialize ()
+ {
+ // Check if system property is set and points to a real file
+ final String loggingProp = System.getProperty(LOGBACK_LOGGING_KEY);
+
+ if (loggingProp != null) {
+ File loggingFile = new File(loggingProp);
+
+ if (loggingFile.exists()) {
+ // Everything seems OK, let LogBack use the config file
+ System.out.println("Using " + loggingFile.getAbsolutePath());
+
+ return;
+ } else {
+ System.out.println(
+ "File " + loggingFile.getAbsolutePath()
+ + " does not exist.");
+ }
+ } else {
+ System.out.println(
+ "Property " + LOGBACK_LOGGING_KEY + " not defined.");
+ }
+
+ // Define a minimal logging configuration, programmatically
+ LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+ Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(
+ Logger.ROOT_LOGGER_NAME);
+
+ // CONSOLE
+ ConsoleAppender consoleAppender = new ConsoleAppender();
+ PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
+ consoleAppender.setName("CONSOLE");
+ consoleAppender.setContext(loggerContext);
+ consoleEncoder.setContext(loggerContext);
+ consoleEncoder.setPattern("%-5level %caller{1} - %msg%ex%n");
+ consoleEncoder.start();
+ consoleAppender.setEncoder(consoleEncoder);
+ consoleAppender.start();
+ root.addAppender(consoleAppender);
+
+ // FILE (located in default temp directory)
+ File logFile;
+ FileAppender fileAppender = new FileAppender();
+ PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
+ fileAppender.setName("FILE");
+ fileAppender.setContext(loggerContext);
+ fileAppender.setAppend(false);
+
+ String now = new SimpleDateFormat("yyyyMMdd'T'HHmmss").format(
+ new Date());
+ logFile = Paths.get(
+ System.getProperty("java.io.tmpdir"),
+ "audiveris-installer-" + now + ".log")
+ .toFile();
+ fileAppender.setFile(logFile.getAbsolutePath());
+ fileEncoder.setContext(loggerContext);
+ fileEncoder.setPattern("%date %level \\(%file:%line\\) - %msg%ex%n");
+ fileEncoder.start();
+ fileAppender.setEncoder(fileEncoder);
+ fileAppender.start();
+ root.addAppender(fileAppender);
+
+ // VIEW (filtered on INFO+)
+ Appender guiAppender = new ViewAppender();
+ guiAppender.setName("VIEW");
+ guiAppender.setContext(loggerContext);
+
+ Filter filter = new Filter()
+ {
+ @Override
+ public FilterReply decide (Object obj)
+ {
+ if (!isStarted()) {
+ return FilterReply.NEUTRAL;
+ }
+
+ LoggingEvent event = (LoggingEvent) obj;
+
+ if (event.getLevel()
+ .toInt() >= Level.INFO_INT) {
+ return FilterReply.NEUTRAL;
+ } else {
+ return FilterReply.DENY;
+ }
+ }
+ };
+
+ filter.start();
+
+ guiAppender.addFilter(filter);
+ guiAppender.start();
+ root.addAppender(guiAppender);
+
+ // Levels
+ root.setLevel(Level.DEBUG);
+
+ Logger jarExpander = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(
+ JarExpander.class);
+ jarExpander.setLevel(Level.INFO);
+
+ // OPTIONAL: print logback internal status messages
+ StatusPrinter.print(loggerContext);
+
+ root.info("Logging to file {}", logFile.getAbsolutePath());
+ }
+}
diff --git a/src/installer/com/audiveris/installer/MessagePanel.java b/src/installer/com/audiveris/installer/MessagePanel.java
new file mode 100644
index 0000000..4d3c942
--- /dev/null
+++ b/src/installer/com/audiveris/installer/MessagePanel.java
@@ -0,0 +1,130 @@
+//----------------------------------------------------------------------------//
+// //
+// M e s s a g e P a n e l //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import ch.qos.logback.classic.Level;
+
+import java.awt.Color;
+import java.awt.Insets;
+
+import javax.swing.JScrollPane;
+import javax.swing.JTextPane;
+import javax.swing.SwingUtilities;
+import javax.swing.text.AbstractDocument;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.SimpleAttributeSet;
+import javax.swing.text.StyleConstants;
+
+/**
+ * Class {@code MessagePanel} handles a panel meant to display log
+ * messages.
+ *
+ * @author Hervé Bitteur
+ */
+public class MessagePanel
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final String LOG_FONT = "Lucida Console";
+
+ //~ Instance fields --------------------------------------------------------
+
+ private final JTextPane textPane = new JTextPane();
+ private final AbstractDocument document = (AbstractDocument) textPane.getStyledDocument();
+ private final SimpleAttributeSet attributes = new SimpleAttributeSet();
+
+ /** Host the text in a scroll pane. */
+ private JScrollPane panel = new JScrollPane();
+
+ //~ Constructors -----------------------------------------------------------
+
+ /**
+ * Creates a new MessagePanel object.
+ */
+ public MessagePanel ()
+ {
+ textPane.setEditable(false);
+ textPane.setMargin(new Insets(5, 5, 5, 5));
+ panel.setViewportView(textPane);
+
+ // Font name & size
+ StyleConstants.setFontFamily(attributes, LOG_FONT);
+ StyleConstants.setFontSize(attributes, 10);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // clearLog //
+ //----------//
+ /**
+ * Clear the display.
+ */
+ public void clear ()
+ {
+ textPane.setText("");
+ textPane.setCaretPosition(0);
+ panel.repaint();
+ }
+
+ //---------//
+ // display //
+ //---------//
+ public void display (final Level level,
+ final String str)
+ {
+ SwingUtilities.invokeLater(
+ new Runnable() {
+ @Override
+ public void run ()
+ {
+ // Color
+ StyleConstants.setForeground(
+ attributes,
+ getLevelColor(level));
+
+ try {
+ document.insertString(
+ document.getLength(),
+ str + "\n",
+ attributes);
+ } catch (BadLocationException ex) {
+ ex.printStackTrace();
+ }
+ }
+ });
+ }
+
+ //--------------//
+ // getComponent //
+ //--------------//
+ public JScrollPane getComponent ()
+ {
+ return panel;
+ }
+
+ //---------------//
+ // getLevelColor //
+ //---------------//
+ private Color getLevelColor (Level level)
+ {
+ if (level.isGreaterOrEqual(Level.ERROR)) {
+ return Color.RED;
+ } else if (level.isGreaterOrEqual(Level.WARN)) {
+ return Color.BLUE;
+ } else if (level.isGreaterOrEqual(Level.INFO)) {
+ return Color.BLACK;
+ } else {
+ return Color.GRAY;
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/OcrCompanion.java b/src/installer/com/audiveris/installer/OcrCompanion.java
new file mode 100644
index 0000000..0903af4
--- /dev/null
+++ b/src/installer/com/audiveris/installer/OcrCompanion.java
@@ -0,0 +1,499 @@
+//----------------------------------------------------------------------------//
+// //
+// O c r C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.swing.JOptionPane;
+
+/**
+ * Class {@code OcrCompanion} handles the installation of Tesseract
+ * library and the support for a selected set of languages.
+ *
+ * @author Hervé Bitteur
+ */
+public class OcrCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ OcrCompanion.class);
+
+ /** Companion description. */
+ private static final String DESC = "You can select which precise languages"
+ + " the OCR should support."
+ + " For this, please use the language selector below.";
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ /** Internet address to retrieve Tesseract trained data. */
+ private static final String TESS_RADIX = "http://tesseract-ocr.googlecode.com/files";
+
+ /** Precise Tesseract version. */
+ private static final String TESS_VERSION = "tesseract-ocr-3.02";
+
+ /**
+ * Collection of all Tesseract supported languages.
+ * Update this list according to Tesseract web site on
+ * http://code.google.com/p/tesseract-ocr/downloads/list
+ */
+ public static final String[] ALL_LANGUAGES = new String[]{
+ "afr", "ara", "aze", "bel",
+ "ben", "bul", "cat", "ces",
+ "chi_sim", "chi_tra", "chr",
+ "dan", "deu", "ell", "eng",
+ "enm", "epo", "epo_alt",
+ "equ", "est", "eus", "fin",
+ "fra", "frk", "frm", "glg",
+ "grc", "heb", "hin", "hrv",
+ "hun", "ind", "isl", "ita",
+ "ita_old", "jpn", "kan",
+ "kor", "lav", "lit", "mal",
+ "mkd", "mlt", "msa", "nld",
+ "nor", "pol", "por", "ron",
+ "rus", "slk", "slv", "spa",
+ "spa_old", "sqi", "srp",
+ "swa", "swe", "tam", "tel",
+ "tgl", "tha", "tur", "ukr",
+ "vie"
+ };
+
+ /** Collection of pre-desired languages. */
+ public static final String[] PREDESIRED_LANGUAGES = new String[]{
+ "deu", "eng", "fra",
+ "ita"
+ };
+
+ /** Name of local temporary folder for OCR languages. */
+ public static final String LOCAL_OCR_FOLDER = "local-ocr-folder";
+
+ /** Name of local temporary folder for binary files. */
+ public static final String LOCAL_LIB_FOLDER = "local-lib-folder";
+
+ //~ Instance fields --------------------------------------------------------
+ /** Handling of tessdata directory. */
+ private final Tessdata tessdata = new Tessdata();
+
+ /** The languages to be added (if not present). */
+ private final Set desired = buildDesiredLanguages();
+
+ /** The languages to be removed (if present). */
+ private final Set nonDesired = new TreeSet();
+
+ /** User selector, if any. */
+ private LangSelector selector;
+
+ //~ Constructors -----------------------------------------------------------
+ //--------------//
+ // OcrCompanion //
+ //--------------//
+ /**
+ * Creates a new OcrCompanion object for OCR library and a list of
+ * supported languages.
+ */
+ public OcrCompanion ()
+ {
+ super("OCR", DESC);
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 60);
+ selector = new LangSelector(this);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // checkInstalled //
+ //----------------//
+ @Override
+ public boolean checkInstalled ()
+ {
+ try {
+ boolean installed = true;
+
+ // Check for Tesseract library
+ if (!descriptor.isTesseractInstalled()) {
+ installed = false;
+ } else {
+ if (selector != null) {
+ selector.update(null);
+ }
+
+ // We check if each of the desired language actually exists
+ for (String language : desired) {
+ if (!isLangInstalled(language)) {
+ installed = false;
+
+ break;
+ }
+ }
+
+ if (installed) {
+ // We check if each of the non-desired language still exists
+ for (String language : nonDesired) {
+ if (isLangInstalled(language)) {
+ installed = false;
+
+ break;
+ }
+ }
+ }
+ }
+
+ status = installed ? Status.INSTALLED : Status.NOT_INSTALLED;
+ } catch (Throwable ex) {
+ logger.warn("Tesseract could not be checked", ex);
+ status = Status.NOT_INSTALLED;
+ }
+
+ return status == Status.INSTALLED;
+ }
+
+ //------------//
+ // getDesired //
+ //------------//
+ /**
+ * Report the set of desired languages.
+ *
+ * @return the current set of desired languages
+ */
+ public Set getDesired ()
+ {
+ return desired;
+ }
+
+ //------------------//
+ // getInstallWeight //
+ //------------------//
+ @Override
+ public int getInstallWeight ()
+ {
+ return isNeeded() ? 1 : 0;
+ }
+
+ //---------------//
+ // getNonDesired //
+ //---------------//
+ /**
+ * Report the set of non-desired languages.
+ *
+ * @return the current set of non-desired languages
+ */
+ public Set getNonDesired ()
+ {
+ return nonDesired;
+ }
+
+ //-------------//
+ // getSelector //
+ //-------------//
+ public LangSelector getSelector ()
+ {
+ return selector;
+ }
+
+ //-----------------//
+ // isLangInstalled //
+ //-----------------//
+ public boolean isLangInstalled (String language)
+ {
+ try {
+ // We check if the main language file actually exists
+ final File lanFile = new File(
+ tessdata.get(),
+ language + ".traineddata");
+
+ return lanFile.exists();
+ } catch (Exception ex) {
+ logger.warn("No tessdata found", ex);
+
+ return false;
+ }
+ }
+
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ // First, install library if so needed
+ if (!descriptor.isTesseractInstalled()) {
+ descriptor.installTesseract();
+ }
+
+ // Then, we process languages in alphabetical order
+ // To keep in sync with selector display
+ final Set relevant = new TreeSet();
+ relevant.addAll(desired);
+ relevant.addAll(nonDesired);
+
+ for (String lang : relevant) {
+ if (desired.contains(lang) && !isLangInstalled(lang)) {
+ if (selector != null) {
+ selector.update(lang);
+ }
+
+ installLanguage(lang);
+ } else if (nonDesired.contains(lang)) {
+ if (selector != null) {
+ selector.update(lang);
+ }
+
+ uninstallLanguage(lang);
+ nonDesired.remove(lang);
+ }
+
+ if (selector != null) {
+ selector.update(null);
+ }
+ }
+
+ // If some languages have been installed, copy them to tessdata
+ final File local = getLocalOcrFolder();
+
+ if (local.exists()
+ && local.isDirectory()
+ && (local.listFiles().length > 0)) {
+ // Try immediate copy, in user mode
+ final Path[] sources = new Path[]{
+ new File(
+ local,
+ Descriptor.TESSERACT_OCR).toPath()
+ };
+ final Path target = tessdata.get()
+ .getParentFile()
+ .getParentFile()
+ .toPath();
+
+ try {
+ FileCopier fc = new FileCopier(sources, target, true);
+ fc.copy();
+ } catch (IOException ex) {
+ logger.debug(
+ "Could not directly copy language files"
+ + ", will post a copy command at system level");
+
+ // Fallback to a posted copy command
+ // Dirty hack since XCOPY (Windows) and cp (Unix) don't treat
+ // source and target similarly when these are directories
+ Path source = null;
+
+ if (DescriptorFactory.WINDOWS) {
+ source = local.toPath();
+ } else if (DescriptorFactory.LINUX) {
+ source = new File(local, Descriptor.TESSERACT_OCR).toPath();
+ }
+
+ // Here, source and target are folders (not regular files)
+ appendCommand(descriptor.getCopyCommand(source, target));
+ }
+ }
+ }
+
+ //-----------------------//
+ // buildDesiredLanguages //
+ //-----------------------//
+ private Set buildDesiredLanguages ()
+ {
+ Set set = new TreeSet();
+
+ // Initialize selected languages with the pre-selected ones
+ set.addAll(Arrays.asList(PREDESIRED_LANGUAGES));
+
+ // Include all the already installed languages as well
+ for (String lang : ALL_LANGUAGES) {
+ if (isLangInstalled(lang)) {
+ set.add(lang);
+ }
+ }
+
+ return set;
+ }
+
+ //-------------------//
+ // getLocalOcrFolder //
+ //-------------------//
+ /**
+ * Report the local (temporary) folder where OCR language data are
+ * expanded before being copied to final target location.
+ *
+ * @return the local OCR folder
+ */
+ private File getLocalOcrFolder ()
+ {
+ return new File(descriptor.getTempFolder(), LOCAL_OCR_FOLDER);
+ }
+
+ //-----------------//
+ // installLanguage //
+ //-----------------//
+ private void installLanguage (final String lang)
+ throws Exception
+ {
+ try {
+ final String tarName = TESS_VERSION + "." + lang + ".tar";
+ final String archiveName = tarName + ".gz";
+ final String archiveHttp = TESS_RADIX + "/" + archiveName;
+
+ // Download
+ final File temp = descriptor.getTempFolder();
+ final File targz = new File(temp, archiveName);
+ Utilities.download(archiveHttp, targz);
+
+ // Decompress the .tar.gz
+ Expander.unGzip(targz, temp);
+
+ // Expand the .tar to LOCAL_OCR_FOLDER
+ final File tar = new File(temp, tarName);
+ logger.debug("tar: {}", tar);
+
+ final File local = getLocalOcrFolder();
+ final File data = new File(
+ new File(local, Descriptor.TESSERACT_OCR),
+ Descriptor.TESSDATA);
+ data.mkdirs();
+ logger.debug("local tessdata folder: {}", data.getAbsolutePath());
+ Expander.unTar(tar, local);
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(
+ Installer.getFrame(),
+ ex.getMessage());
+ throw ex;
+ }
+ }
+
+ //-------------------//
+ // uninstallLanguage //
+ //-------------------//
+ private void uninstallLanguage (final String lang)
+ throws Exception
+ {
+ if (!tessdata.get()
+ .exists()) {
+ return;
+ }
+
+ // Clean up relevant files in the folder
+ Files.walkFileTree(
+ tessdata.get().toPath(),
+ EnumSet.noneOf(FileVisitOption.class),
+ 1,
+ new SimpleFileVisitor()
+ {
+ @Override
+ public FileVisitResult visitFile (Path file,
+ BasicFileAttributes attrs)
+ throws IOException
+ {
+ Path name = file.getName(file.getNameCount() - 1);
+ logger.debug("Visiting {}, name {}", file, name);
+
+ if (name.toString()
+ .startsWith(lang + ".")) {
+ logger.info("Removing file {}", file);
+
+ try {
+ // Try immediate delete
+ Files.delete(file);
+ } catch (AccessDeniedException ex) {
+ logger.info(
+ "Cannot delete {}, will post a delete command",
+ file);
+
+ // Post a delete command
+ appendCommand(
+ descriptor.getDeleteCommand(file));
+ }
+ }
+
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //----------//
+ // Tessdata //
+ //----------//
+ /**
+ * Handles the precise location of tessdata.
+ */
+ private static class Tessdata
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private File prefixDir;
+
+ private File tessdataDir;
+
+ //~ Methods ------------------------------------------------------------
+ /**
+ * Report tessdata directory specification (there is no
+ * guarantee that the directory actually exists).
+ * If environment variable TESSDATA_PREFIX exists, it is used as the
+ * path to tessdata parent directory.
+ * Otherwise, we use the OS-dependent default prefix.
+ *
+ * @return the tessdata directory
+ */
+ public File get ()
+ {
+ if (tessdataDir == null) {
+ tessdataDir = new File(getPrefix(), Descriptor.TESSDATA);
+ }
+
+ return tessdataDir;
+ }
+
+ /**
+ * Report the parent folder of tessdata, usually the one
+ * pointed to by TESSDATA_PREFIX (there is no guarantee that
+ * the directory exists or is writable).
+ *
+ * @return the tessdata PARENT directory
+ */
+ public File getPrefix ()
+ {
+ if (prefixDir == null) {
+ final String prefix = System.getenv(Descriptor.TESSDATA_PREFIX);
+
+ if (prefix == null) {
+ prefixDir = descriptor.getDefaultTessdataPrefix();
+ } else {
+ prefixDir = new File(prefix);
+ }
+ }
+
+ return prefixDir;
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/PluginsCompanion.java b/src/installer/com/audiveris/installer/PluginsCompanion.java
new file mode 100644
index 0000000..ab5e127
--- /dev/null
+++ b/src/installer/com/audiveris/installer/PluginsCompanion.java
@@ -0,0 +1,91 @@
+//----------------------------------------------------------------------------//
+// //
+// P l u g i n s C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+
+/**
+ * Class {@code PluginsCompanion} handles the local installation of
+ * JavaScript plugins.
+ *
+ * @author Hervé Bitteur
+ */
+public class PluginsCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ PluginsCompanion.class);
+
+ /** Tooltip. */
+ private static final String DESC = "[This component is optional ]"
+ + " It installs JavaScript plugins"
+ + " which can be customized manually.";
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ //~ Constructors -----------------------------------------------------------
+ //------------------//
+ // PluginsCompanion //
+ //------------------//
+ /**
+ * Creates a new PluginsCompanion object.
+ */
+ public PluginsCompanion ()
+ {
+ super("Plugins", DESC);
+ need = Need.SELECTED;
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 90);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ final URI codeBase = Jnlp.basicService.getCodeBase()
+ .toURI();
+
+ final URL url = Utilities.toURI(codeBase, "resources/plugins.jar")
+ .toURL();
+
+ Utilities.downloadJarAndExpand(
+ "Plugins",
+ url.toString(),
+ descriptor.getTempFolder(),
+ "plugins",
+ makeTargetFolder());
+ }
+
+ //-----------------//
+ // getTargetFolder //
+ //-----------------//
+ @Override
+ protected File getTargetFolder ()
+ {
+ return new File(descriptor.getConfigFolder(), "plugins");
+ }
+}
diff --git a/src/installer/com/audiveris/installer/RegexUtil.java b/src/installer/com/audiveris/installer/RegexUtil.java
new file mode 100644
index 0000000..02965d7
--- /dev/null
+++ b/src/installer/com/audiveris/installer/RegexUtil.java
@@ -0,0 +1,79 @@
+//----------------------------------------------------------------------------//
+// //
+// R e g e x U t i l //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+
+package com.audiveris.installer;
+
+import java.util.regex.Matcher;
+
+/**
+ * Class {@code RegexUtil} gathers utility features related to Regex.
+ *
+ * @author Hervé Bitteur
+ */
+public class RegexUtil
+{
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // getGroup //
+ //----------//
+ /**
+ * Report the input sequence captured by the provided
+ * named-capturing group.
+ *
+ * @param matcher the matcher
+ * @param name the provided name for desired group
+ * @return the input sequence, perhaps empty but not null
+ */
+ public static String getGroup (Matcher matcher,
+ String name)
+ {
+ String result = null;
+
+ try {
+ result = matcher.group(name);
+ } catch (Exception ignored) {
+ }
+
+ if (result != null) {
+ return result;
+ } else {
+ return "";
+ }
+ }
+
+ //-------//
+ // group //
+ //-------//
+ /**
+ * Convenient method to build a named-group like "(?content)"
+ *
+ * @param name name for the group
+ * @param content inner content of the group
+ * @return the ready to use group value
+ */
+ public static String group (String name,
+ String content)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("(?<");
+ sb.append(name);
+ sb.append(">");
+
+ sb.append(content);
+
+ sb.append(")");
+
+ return sb.toString();
+ }
+}
diff --git a/src/installer/com/audiveris/installer/SpecificFile.java b/src/installer/com/audiveris/installer/SpecificFile.java
new file mode 100644
index 0000000..d6256bf
--- /dev/null
+++ b/src/installer/com/audiveris/installer/SpecificFile.java
@@ -0,0 +1,53 @@
+//----------------------------------------------------------------------------//
+// //
+// S p e c i f i c F i l e //
+// //
+//----------------------------------------------------------------------------//
+//
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved.
+// This software is released under the GNU General Public License.
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions.
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+/**
+ * Class {@code SpecificFile} is used to govern the installation of
+ * a specific file in proper target location.
+ * All source files are provided in a common .jar file ("specifics.jar" as
+ * found in installer /resources folder).
+ * The purpose of this class is to define the source folder and name (in the
+ * common .jar file) and the target folder and name (in the user machine).
+ *
+ * @author Hervé Bitteur
+ */
+public class SpecificFile
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /** Full resource name in source archive. */
+ public final String source;
+
+ /** Full target file path on user machine. */
+ public final String target;
+
+ /** Target to be set as executable. */
+ public final boolean isExec;
+
+ //~ Constructors -----------------------------------------------------------
+ /**
+ * Creates a new SpecificFile object.
+ *
+ * @param source full resource name in source archive
+ * @param target full target path on user machine
+ * @param isExec true if target is to be set as executable
+ */
+ public SpecificFile (String source,
+ String target,
+ boolean isExec)
+ {
+ this.source = source;
+ this.target = target;
+ this.isExec = isExec;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/TrainingCompanion.java b/src/installer/com/audiveris/installer/TrainingCompanion.java
new file mode 100644
index 0000000..81dc18e
--- /dev/null
+++ b/src/installer/com/audiveris/installer/TrainingCompanion.java
@@ -0,0 +1,91 @@
+//----------------------------------------------------------------------------//
+// //
+// T r a i n i n g C o m p a n i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+
+/**
+ * Class {@code TrainingCompanion} handles the installation of glyph
+ * samples used for (re)training Audiveris classifier.
+ *
+ * @author Hervé Bitteur
+ */
+public class TrainingCompanion
+ extends AbstractCompanion
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ TrainingCompanion.class);
+
+ /** Tooltip. */
+ private static final String DESC = "[This component is optional ]"
+ + " It installs samples that allow"
+ + " to retrain the shape classifier.";
+
+ /** Environment descriptor. */
+ private static final Descriptor descriptor = DescriptorFactory.getDescriptor();
+
+ //~ Constructors -----------------------------------------------------------
+ //-------------------//
+ // TrainingCompanion //
+ //-------------------//
+ /**
+ * Creates a new TrainingCompanion object.
+ */
+ public TrainingCompanion ()
+ {
+ super("Training", DESC);
+ need = Need.NOT_SELECTED;
+
+ if (Installer.hasUI()) {
+ view = new BasicCompanionView(this, 95);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------//
+ // doInstall //
+ //-----------//
+ @Override
+ protected void doInstall ()
+ throws Exception
+ {
+ final URI codeBase = Jnlp.basicService.getCodeBase()
+ .toURI();
+
+ final URL url = Utilities.toURI(codeBase, "resources/train.jar")
+ .toURL();
+
+ Utilities.downloadJarAndExpand(
+ "Training",
+ url.toString(),
+ descriptor.getTempFolder(),
+ "train",
+ makeTargetFolder());
+ }
+
+ //-----------------//
+ // getTargetFolder //
+ //-----------------//
+ @Override
+ protected File getTargetFolder ()
+ {
+ return new File(descriptor.getDataFolder(), "train");
+ }
+}
diff --git a/src/installer/com/audiveris/installer/TreeRemover.java b/src/installer/com/audiveris/installer/TreeRemover.java
new file mode 100644
index 0000000..6081611
--- /dev/null
+++ b/src/installer/com/audiveris/installer/TreeRemover.java
@@ -0,0 +1,80 @@
+//----------------------------------------------------------------------------//
+// //
+// T r e e R e m o v e r //
+// //
+//----------------------------------------------------------------------------//
+//
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved.
+// This software is released under the GNU General Public License.
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions.
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+/**
+ * Class {@code TreeRemover} is a FileVisitor that deletes a folder
+ * recursively.
+ *
+ * @author Hervé Bitteur
+ */
+public class TreeRemover
+ extends SimpleFileVisitor
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final Logger logger = LoggerFactory.getLogger(
+ TreeRemover.class);
+
+ //~ Methods ----------------------------------------------------------------
+ @Override
+ public FileVisitResult postVisitDirectory (Path dir,
+ IOException ex)
+ throws IOException
+ {
+ if (ex == null) {
+ logger.debug("Deleting directory {}", dir);
+ Files.delete(dir);
+
+ return FileVisitResult.CONTINUE;
+ } else {
+ // directory iteration failed
+ throw ex;
+ }
+ }
+
+ //--------//
+ // remove //
+ //--------//
+ /**
+ * Convenient method to remove a folder recursively.
+ *
+ * @param path the folder to remove, with all its content
+ * @throws IOException
+ */
+ public static void remove (Path path)
+ throws IOException
+ {
+ Files.walkFileTree(path, new TreeRemover());
+ }
+
+ @Override
+ public FileVisitResult visitFile (Path file,
+ BasicFileAttributes attrs)
+ throws IOException
+ {
+ logger.debug("Deleting file {}", file);
+ Files.delete(file);
+
+ return FileVisitResult.CONTINUE;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java b/src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java
new file mode 100644
index 0000000..f598ddf
--- /dev/null
+++ b/src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java
@@ -0,0 +1,64 @@
+//----------------------------------------------------------------------------//
+// //
+// U n s u p p o r t e d E n v i r o n m e n t E x c e p t i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+
+/**
+ * Class {@code UnsupportedEnvironmentException} is used to signal
+ * that the environment is not supported by the installer.
+ *
+ * @author Hervé Bitteur
+ */
+public class UnsupportedEnvironmentException
+ extends RuntimeException
+{
+ //~ Constructors -----------------------------------------------------------
+
+ /**
+ * Creates a new UnsupportedEnvironmentException object.
+ */
+ public UnsupportedEnvironmentException ()
+ {
+ }
+
+ /**
+ * Creates a new UnsupportedEnvironmentException object.
+ *
+ * @param message DOCUMENT ME!
+ */
+ public UnsupportedEnvironmentException (String message)
+ {
+ super(message);
+ }
+
+ /**
+ * Creates a new UnsupportedEnvironmentException object.
+ *
+ * @param cause DOCUMENT ME!
+ */
+ public UnsupportedEnvironmentException (Throwable cause)
+ {
+ super(cause);
+ }
+
+ /**
+ * Creates a new UnsupportedEnvironmentException object.
+ *
+ * @param message DOCUMENT ME!
+ * @param cause DOCUMENT ME!
+ */
+ public UnsupportedEnvironmentException (String message,
+ Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/src/installer/com/audiveris/installer/Utilities.java b/src/installer/com/audiveris/installer/Utilities.java
new file mode 100644
index 0000000..5a5b694
--- /dev/null
+++ b/src/installer/com/audiveris/installer/Utilities.java
@@ -0,0 +1,322 @@
+//----------------------------------------------------------------------------//
+// //
+// U t i l i t i e s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.JarFile;
+
+/**
+ * Class {@code Utilities} gathers helper methods for installation
+ * whatever the current environment.
+ *
+ * @author Hervé Bitteur
+ */
+public class Utilities
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ Utilities.class);
+
+ //~ Methods ----------------------------------------------------------------
+ //----------//
+ // download //
+ //----------//
+ /**
+ * Download data from a given url to the specified target file.
+ *
+ * @param urlString the url to download from
+ * @param target the target file
+ */
+ public static void download (String urlString,
+ File target)
+ {
+ // Check url
+ URL url;
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException ex) {
+ logger.warn("MalformedURLException", ex);
+ throw new RuntimeException(ex);
+ }
+
+ // Check target
+ File parentDir = target.getParentFile();
+ if (!parentDir.exists()) {
+ throw new RuntimeException("Target directory of " + target
+ + " does not exist");
+ }
+ if (!parentDir.isDirectory()) {
+ throw new RuntimeException(parentDir + " is not a directory");
+ }
+
+ logger.info("Downloading {} to {} ...", url, target.getAbsolutePath());
+ Jnlp.extensionInstallerService.setStatus("Downloading " + url);
+
+ try {
+ URLConnection uc = url.openConnection();
+ uc.setUseCaches(true);
+
+ try (InputStream is = uc.getInputStream()) {
+ Files.copy(is, target.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ logger.debug("End of download.");
+ Jnlp.extensionInstallerService.setStatus("");
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ex) {
+ logger.warn("Error on Thread.sleep", ex);
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException("Error downloading " + urlString
+ + " to " + target.getAbsolutePath(), ex);
+ }
+ }
+
+ //------------------------//
+ // downloadExecAndInstall //
+ //------------------------//
+ /**
+ * Download an executable from provided url and launch the
+ * executable with the installOption.
+ *
+ * @param title A title for the software to process
+ * @param url url to download from
+ * @param temp temp directory to be used for download
+ * @param installOption installation option if any, perhaps empty but not
+ * null
+ * @throws Throwable
+ */
+ public static void downloadExecAndInstall (String title,
+ String url,
+ File temp,
+ String installOption)
+ throws Exception
+ {
+ // Download
+ final String fileName = new File(url).getName();
+ final File exec = new File(temp, fileName);
+ download(url, exec);
+
+ // Install
+ final List output = new ArrayList<>();
+ try {
+ final int res = Utilities.runProcess(
+ output, exec.getAbsolutePath(), installOption);
+ if (res != 0) {
+ if (Installer.isAdmin) {
+ final String lines = Utilities.dumpOfLines(output);
+ logger.warn(lines);
+ throw new RuntimeException("Could not run " + exec
+ + ", exit: " + res + "\n" + lines);
+ } else {
+ postExec(exec, installOption);
+ }
+ }
+ } catch (IOException | InterruptedException | RuntimeException ex) {
+ if (Installer.isAdmin) {
+ logger.warn("Could not install " + title, ex);
+ throw ex;
+ } else {
+ postExec(exec, installOption);
+ }
+ }
+ }
+
+ //----------//
+ // postExec //
+ //----------//
+ /**
+ * Post the launching of a program
+ *
+ * @param exec path to the executable
+ * @param options options, if any
+ */
+ private static void postExec (File exec,
+ String... options)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\"").append(exec.getAbsolutePath()).append("\"");
+ for (String option : options) {
+ sb.append(" ").append(option);
+ }
+
+ Installer.getBundle().appendCommand(sb.toString());
+ }
+
+ //----------------------//
+ // downloadJarAndExpand //
+ //----------------------//
+ /**
+ * Download a .jar file from a provided url and expand it to the
+ * desired target directory.
+ *
+ * @param title A title for the item to process
+ * @param url url to download from
+ * @param temp temp directory to be used for download
+ * @param root root filter
+ * @param targetDir target directory
+ * @throws Throwable
+ */
+ public static void downloadJarAndExpand (String title,
+ String url,
+ File temp,
+ String root,
+ File targetDir)
+ throws Exception
+ {
+ // Download the .jar file
+ final String fileName = new File(url).getName();
+ final File jarFile = new File(temp, fileName);
+ download(url, jarFile);
+ JarFile jar = new JarFile(jarFile);
+
+ // Expand the jar file
+ if (!targetDir.exists()) {
+ if (targetDir.mkdirs()) {
+ logger.info("Created folder {}", targetDir.getAbsolutePath());
+ }
+ }
+ JarExpander exp = new JarExpander(jar, root, targetDir);
+ exp.install();
+ }
+
+ //------------//
+ // runProcess //
+ //------------//
+ /**
+ * Launch a process.
+ *
+ * @param output (output) the output lines (stdout and stderr)
+ * @param args (input) executable and arguments
+ * @return the process exit code
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ public static int runProcess (List output,
+ String... args)
+ throws IOException, InterruptedException
+ {
+ // Command arguments
+ List cmdArgs = Arrays.asList(args);
+ logger.debug("runProcess args: {}", cmdArgs);
+
+ try {
+ // Spawn process
+ ProcessBuilder pb = new ProcessBuilder(cmdArgs);
+ pb.redirectErrorStream(true);
+
+ Process process = pb.start();
+
+ // Read output
+ InputStream is = process.getInputStream();
+ InputStreamReader isr = new InputStreamReader(is);
+ BufferedReader br = new BufferedReader(isr);
+ String line;
+
+ while ((line = br.readLine()) != null) {
+ logger.debug("line: {}", line);
+ output.add(line);
+ }
+
+ // Wait for process completion
+ int exitValue = process.waitFor();
+ logger.debug("Exit value is: {}", exitValue);
+
+ return exitValue;
+ } catch (Exception ex) {
+ logger.warn("[isAdmin: " + Installer.isAdmin
+ + "] Error running " + cmdArgs, ex);
+ throw ex;
+ }
+ }
+
+ //-------//
+ // toURI //
+ //-------//
+ /**
+ * Convenient method to simulate a parent/child composition
+ *
+ * @param parent the URI to parent directory
+ * @param child the child name
+ * @return the resulting URI
+ */
+ public static URI toURI (URI parent,
+ String child)
+ {
+ try {
+ // Make sure parent ends with a '/'
+ if (parent == null) {
+ throw new IllegalArgumentException("Parent is null");
+ }
+
+ StringBuilder dirName = new StringBuilder(parent.toString());
+
+ if (dirName.charAt(dirName.length() - 1) != '/') {
+ dirName.append('/');
+ }
+
+ // Make sure child does not start with a '/'
+ if ((child == null) || child.isEmpty()) {
+ throw new IllegalArgumentException("Child is null or empty");
+ }
+
+ if (child.startsWith("/")) {
+ throw new IllegalArgumentException(
+ "Child is absolute: " + child);
+ }
+
+ return new URI(dirName.append(child).toString());
+ } catch (URISyntaxException ex) {
+ throw new IllegalArgumentException(ex.getMessage(), ex);
+ }
+ }
+
+ //-------------//
+ // dumpOfLines //
+ //-------------//
+ /**
+ * Meant for dumping a bunch of lines
+ *
+ * @param lines the lines to dump
+ * @return one string for all lines
+ */
+ public static String dumpOfLines (List lines)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ for (String line : lines) {
+ sb.append("\n>>> ").append(line);
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/installer/com/audiveris/installer/ViewAppender.java b/src/installer/com/audiveris/installer/ViewAppender.java
new file mode 100644
index 0000000..3f68b5a
--- /dev/null
+++ b/src/installer/com/audiveris/installer/ViewAppender.java
@@ -0,0 +1,84 @@
+//----------------------------------------------------------------------------//
+// //
+// V i e w A p p e n d e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class {@code ViewAppender} is a log appender that appends the
+ * logging messages to the BundleView.
+ *
+ * @author Hervé Bitteur
+ */
+public class ViewAppender
+ extends AppenderBase
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /** The installer bundle view. */
+ private BundleView view;
+
+ /** Needed to store messages until the view gets available. */
+ private final List backlog = new ArrayList();
+
+ //~ Methods ----------------------------------------------------------------
+
+ //--------//
+ // append //
+ //--------//
+ @Override
+ protected void append (ILoggingEvent event)
+ {
+ if (!isReady()) {
+ backlog.add(event);
+ } else {
+ if (!backlog.isEmpty()) {
+ for (ILoggingEvent evt : backlog) {
+ publish(evt);
+ }
+
+ backlog.clear();
+ }
+
+ publish(event);
+ }
+ }
+
+ //---------//
+ // isReady //
+ //---------//
+ private boolean isReady ()
+ {
+ if (Installer.getBundle() == null) {
+ return false;
+ }
+
+ view = Installer.getBundle()
+ .getView();
+
+ return view != null;
+ }
+
+ //---------//
+ // publish //
+ //---------//
+ private void publish (ILoggingEvent event)
+ {
+ Level level = event.getLevel();
+ view.publishMessage(level, event.getFormattedMessage());
+ }
+}
diff --git a/src/installer/com/audiveris/installer/mac/package.html b/src/installer/com/audiveris/installer/mac/package.html
new file mode 100644
index 0000000..2e50158
--- /dev/null
+++ b/src/installer/com/audiveris/installer/mac/package.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Package com.audiveris.installer.mac
+
+
+
+
+ Specific implementations for Mac environment.
+
+
+
+
diff --git a/src/installer/com/audiveris/installer/package.html b/src/installer/com/audiveris/installer/package.html
new file mode 100644
index 0000000..93ae0ff
--- /dev/null
+++ b/src/installer/com/audiveris/installer/package.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Package com.audiveris.installer
+
+
+
+
+ Core installation features, regardless of target OS.
+
+
+
diff --git a/src/installer/com/audiveris/installer/unix/Package.java b/src/installer/com/audiveris/installer/unix/Package.java
new file mode 100644
index 0000000..d8c5fc9
--- /dev/null
+++ b/src/installer/com/audiveris/installer/unix/Package.java
@@ -0,0 +1,186 @@
+//----------------------------------------------------------------------------//
+// //
+// P a c k a g e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer.unix;
+
+import com.audiveris.installer.Installer;
+import com.audiveris.installer.RegexUtil;
+import com.audiveris.installer.Utilities;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class {@code Package} handles a Linux package requirement and
+ * installation, using the underlying "apt" and dpkg" utilities.
+ *
+ * @author Hervé Bitteur
+ */
+public class Package
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /**
+ * Usual logger utility
+ */
+ private static final Logger logger = LoggerFactory.getLogger(Package.class);
+
+ //~ Instance fields --------------------------------------------------------
+ /**
+ * Name of the package.
+ */
+ public final String name;
+
+ /**
+ * Minimum version required.
+ */
+ public final VersionNumber minVersion;
+
+ //~ Constructors -----------------------------------------------------------
+ //---------//
+ // Package //
+ //---------//
+ /**
+ * Create a Package instance.
+ *
+ * @param name the package name
+ * @param minVersion the minimum required version
+ */
+ public Package (String name,
+ String minVersion)
+ {
+ if ((name == null) || name.isEmpty()) {
+ throw new IllegalArgumentException("Null or empty package name");
+ }
+
+ if ((minVersion == null) || minVersion.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Null or empty package minimum version");
+ }
+
+ this.name = name;
+ this.minVersion = new VersionNumber(minVersion);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //---------------------//
+ // getInstalledVersion //
+ //---------------------//
+ /**
+ * Report the version number of this package, if installed.
+ *
+ * @return the version number, or null if not found
+ */
+ public VersionNumber getInstalledVersion ()
+ {
+ final String VERSION = "version";
+ final Pattern versionPattern = Pattern.compile(
+ "^Version: " + RegexUtil.group(VERSION, ".*") + "$");
+
+ final String STATUS = "status";
+ final Pattern statusPattern = Pattern.compile(
+ "^Status: " + RegexUtil.group(STATUS, ".*") + "$");
+
+ try {
+ List output = new ArrayList();
+ int res = Utilities.runProcess(
+ output,
+ "bash",
+ "-c",
+ "LANG=en_US dpkg -s " + name);
+
+ if (res != 0) {
+ // If we get a non-zero exit code no info can be retrieved
+ if (!output.isEmpty()) {
+ final String lines = Utilities.dumpOfLines(output);
+ logger.info(lines);
+ } else {
+ logger.debug("No command output");
+ }
+
+ return null;
+ } else {
+ // Check status
+ boolean installed = false;
+
+ for (String line : output) {
+ Matcher matcher = statusPattern.matcher(line);
+
+ if (matcher.matches()) {
+ String statusStr = RegexUtil.getGroup(matcher, STATUS);
+
+ if (statusStr.equals("install ok installed")) {
+ installed = true;
+
+ break;
+ }
+ }
+ }
+
+ if (!installed) {
+ return null;
+ }
+
+ // Return installed version
+ for (String line : output) {
+ Matcher matcher = versionPattern.matcher(line);
+
+ if (matcher.matches()) {
+ String versionStr = RegexUtil.getGroup(
+ matcher,
+ VERSION);
+
+ return new VersionNumber(versionStr);
+ }
+ }
+ }
+ } catch (Throwable ex) {
+ logger.warn("Could not get package version", ex);
+ }
+
+ return null;
+ }
+
+ //---------//
+ // install //
+ //---------//
+ /**
+ * Install the latest version of this package
+ *
+ * @throws Exception
+ */
+ public void install ()
+ throws Exception
+ {
+ String cmd = "apt-get install -y " + name;
+ Installer.getBundle()
+ .appendCommand(cmd);
+ }
+
+ //-------------//
+ // isInstalled //
+ //-------------//
+ public boolean isInstalled ()
+ {
+ VersionNumber installedVersion = getInstalledVersion();
+
+ if (installedVersion == null) {
+ return false;
+ }
+
+ return installedVersion.compareTo(minVersion) >= 0;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/unix/UnixDescriptor.java b/src/installer/com/audiveris/installer/unix/UnixDescriptor.java
new file mode 100644
index 0000000..84fa752
--- /dev/null
+++ b/src/installer/com/audiveris/installer/unix/UnixDescriptor.java
@@ -0,0 +1,424 @@
+//----------------------------------------------------------------------------//
+// //
+// U n i x D e s c r i p t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer.unix;
+
+import com.audiveris.installer.Descriptor;
+import com.audiveris.installer.SpecificFile;
+import com.audiveris.installer.Utilities;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class {@code UnixDescriptor} implements Installer descriptor for
+ * Linux Ubuntu (32 and 64 bits)
+ *
+ * @author Hervé Bitteur
+ */
+public class UnixDescriptor
+ implements Descriptor
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final Logger logger = LoggerFactory.getLogger(
+ UnixDescriptor.class);
+
+ /**
+ * Specific prefix for application folders. {@value}
+ */
+ private static final String TOOL_PREFIX = "/" + COMPANY_ID + "/"
+ + TOOL_NAME;
+
+ /**
+ * Set of requirements for c/c++.
+ */
+ private static final Package[] cReqs = new Package[]{
+ new Package("libc6", "2.15"),
+ new Package("libgcc1", "4.6.3"),
+ new Package(
+ "libstdc++6",
+ "4.6.3")
+ };
+
+ /**
+ * Requirement for ghostscript.
+ */
+ private static final Package gsReq = new Package(
+ "ghostscript",
+ "9.06~dfsg");
+
+ /**
+ * Requirement for tesseract.
+ */
+ private static final Package tessReq = new Package("libtesseract3", "3.02");
+
+ /**
+ * KDE front-end for sudo.
+ */
+ private static final String KDESUDO = "kdesudo";
+
+ /**
+ * GTK+ front-end for sudo.
+ */
+ private static final String GKSUDO = "gksudo";
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------------//
+ // getConfigFolder //
+ //-----------------//
+ @Override
+ public File getConfigFolder ()
+ {
+ String config = System.getenv("XDG_CONFIG_HOME");
+
+ if (config != null) {
+ return new File(config + TOOL_PREFIX);
+ }
+
+ String home = System.getenv("HOME");
+
+ if (home != null) {
+ return new File(home + "/.config" + TOOL_PREFIX);
+ }
+
+ throw new RuntimeException("HOME environment variable is not set");
+ }
+
+ //----------------//
+ // getCopyCommand //
+ //----------------//
+ @Override
+ public String getCopyCommand (Path source,
+ Path target)
+ {
+ return "cp -r -v \"" + source.toAbsolutePath() + "\" \""
+ + target.toAbsolutePath() + "\"";
+ }
+
+ //---------------//
+ // getDataFolder //
+ //---------------//
+ @Override
+ public File getDataFolder ()
+ {
+ String data = System.getenv("XDG_DATA_HOME");
+
+ if (data != null) {
+ return new File(data + TOOL_PREFIX);
+ }
+
+ String home = System.getenv("HOME");
+
+ if (home != null) {
+ return new File(home + "/.local/share" + TOOL_PREFIX);
+ }
+
+ throw new RuntimeException("HOME environment variable is not set");
+ }
+
+ //--------------------------//
+ // getDefaultTessdataPrefix //
+ //--------------------------//
+ @Override
+ public File getDefaultTessdataPrefix ()
+ {
+ return new File("/usr/share/" + Descriptor.TESSERACT_OCR + "/");
+ }
+
+ //------------------//
+ // getDeleteCommand //
+ //------------------//
+ @Override
+ public String getDeleteCommand (Path file)
+ {
+ return "rm -v -f \"" + file.toAbsolutePath() + "\"";
+ }
+
+ //-----------------//
+ // getMkdirCommand //
+ //-----------------//
+ @Override
+ public String getMkdirCommand (Path dir)
+ {
+ return "mkdir -v -p \"" + dir.toAbsolutePath() + "\"";
+ }
+
+ //-------------------//
+ // getSetExecCommand //
+ //-------------------//
+ @Override
+ public String getSetExecCommand (Path file)
+ {
+ return "chmod -v a+x \"" + file.toAbsolutePath() + "\"";
+ }
+
+ //------------------//
+ // getSpecificFiles //
+ //------------------//
+ @Override
+ public List getSpecificFiles ()
+ {
+ return Arrays.asList(
+ new SpecificFile("unix/audiveris.sh", "/usr/bin/audiveris", true),
+ new SpecificFile(
+ "unix/AddPlugins.sh",
+ "/usr/share/audiveris/AddPlugins.sh",
+ true));
+ }
+
+ //---------------//
+ // getTempFolder //
+ //---------------//
+ @Override
+ public File getTempFolder ()
+ {
+ final File folder = new File(getDataFolder(), "temp/installation");
+ logger.debug("getTempFolder: {}", folder.getAbsolutePath());
+
+ return folder;
+ }
+
+ //------------//
+ // installCpp //
+ //------------//
+ @Override
+ public void installCpp ()
+ throws Exception
+ {
+ for (Package pkg : cReqs) {
+ if (!pkg.isInstalled()) {
+ pkg.install();
+ }
+ }
+ }
+
+ //--------------------//
+ // installGhostscript //
+ //--------------------//
+ @Override
+ public void installGhostscript ()
+ throws Exception
+ {
+ gsReq.install();
+ }
+
+ //------------------//
+ // installTesseract //
+ //------------------//
+ @Override
+ public void installTesseract ()
+ throws Exception
+ {
+ tessReq.install();
+ }
+
+ //---------//
+ // isAdmin //
+ //---------//
+ @Override
+ public boolean isAdmin ()
+ {
+ // whoami -> herve
+ // sudo whoami -> root
+ try {
+ List output = new ArrayList();
+ int res = Utilities.runProcess(
+ output,
+ "bash",
+ "-c",
+ "whoami");
+
+ if (res != 0) {
+ final String lines = Utilities.dumpOfLines(output);
+ logger.warn(lines);
+ throw new RuntimeException(
+ "Failure in isAdmin(). exit: " + res + "\n" + lines);
+ } else {
+ return !output.isEmpty() && output.get(0)
+ .equals("root");
+ }
+ } catch (Exception ex) {
+ logger.warn("Failure in isAdmin(). ", ex);
+
+ return true; // Safer
+ }
+ }
+
+ //----------------//
+ // isCppInstalled //
+ //----------------//
+ @Override
+ public boolean isCppInstalled ()
+ {
+ for (Package pkg : cReqs) {
+ if (!pkg.isInstalled()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ //------------------------//
+ // isGhostscriptInstalled //
+ //------------------------//
+ @Override
+ public boolean isGhostscriptInstalled ()
+ {
+ return gsReq.isInstalled();
+ }
+
+ //----------------------//
+ // isTesseractInstalled //
+ //----------------------//
+ @Override
+ public boolean isTesseractInstalled ()
+ {
+ return tessReq.isInstalled();
+ }
+
+ //----------//
+ // runShell //
+ //----------//
+ @Override
+ public boolean runShell (boolean asAdmin,
+ List commands)
+ throws Exception
+ {
+ // Build a single compound command
+ StringBuilder sb = new StringBuilder();
+
+ for (String command : commands) {
+ if (sb.length() > 0) {
+ sb.append(" ; ");
+ }
+
+ sb.append(command);
+ }
+
+ String cmdLine = sb.toString();
+
+ // If we have to run as Admin, pick up proper sudo front-end
+ String sudoFrontend = "";
+
+ if (asAdmin) {
+ for (String name : new String[]{GKSUDO, KDESUDO}) {
+ if (isKnown(name)) {
+ sudoFrontend = name;
+ logger.info("\nUsing {}", sudoFrontend);
+ break;
+ }
+ }
+ }
+
+ List output = new ArrayList();
+
+ try {
+ final int res;
+ switch (sudoFrontend) {
+ case GKSUDO:
+ res = Utilities.runProcess(
+ output,
+ GKSUDO,
+ "-DAudiveris installer",
+ "bash -v -e -c '" + cmdLine + "'");
+ break;
+
+ case KDESUDO:
+ res = Utilities.runProcess(
+ output,
+ KDESUDO,
+ "bash -v -e -c '" + cmdLine + "'");
+ break;
+
+ default:
+ res = Utilities.runProcess(
+ output,
+ "bash",
+ "-v",
+ "-e",
+ "-c",
+ cmdLine);
+ }
+
+ if (res == 0) {
+ return true; // Normal exit
+ } else {
+ output.add("Exit code = " + res);
+ }
+ } catch (Exception ex) {
+ logger.warn("Exception in runProcess", ex);
+ }
+
+ // If we are getting here, we failed!
+ final String lines = Utilities.dumpOfLines(output);
+
+ logger.warn(lines);
+
+ throw new RuntimeException(
+ "Failure in runShell().\n" + lines);
+ }
+
+ //---------//
+ // isKnown //
+ //---------//
+ /**
+ * Check whether the provided command name is known
+ *
+ * @param name the command name
+ * @return true if known, false otherwise
+ */
+ private boolean isKnown (String name)
+ {
+ List output = new ArrayList();
+ try {
+ int res = Utilities.runProcess(
+ output,
+ "bash",
+ "-c",
+ "which " + name);
+ logger.debug(Utilities.dumpOfLines(output));
+ return res == 0;
+ } catch (Exception ex) {
+ final String lines = Utilities.dumpOfLines(output);
+ logger.warn("Failure in isKnown(). ex: " + ex + "\n" + lines);
+ return false;
+ }
+ }
+
+ //---------------//
+ // setExecutable //
+ //---------------//
+ @Override
+ public void setExecutable (Path file)
+ throws Exception
+ {
+ List output = new ArrayList();
+ int res = Utilities.runProcess(
+ output,
+ "bash",
+ "-c",
+ "chmod -v a+x \"" + file.toAbsolutePath() + "\"");
+
+ if (res != 0) {
+ final String lines = Utilities.dumpOfLines(output);
+ logger.warn(lines);
+ throw new RuntimeException("Failure in setExecutable().\n" + lines);
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/unix/UnixUtilities.java b/src/installer/com/audiveris/installer/unix/UnixUtilities.java
new file mode 100644
index 0000000..34b83f5
--- /dev/null
+++ b/src/installer/com/audiveris/installer/unix/UnixUtilities.java
@@ -0,0 +1,92 @@
+//----------------------------------------------------------------------------//
+// //
+// U n i x U t i l i t i e s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer.unix;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+/**
+ * Class {@code UnixUtilities} gathers basic utilities for Unix.
+ *
+ * @author Hervé Bitteur
+ */
+public class UnixUtilities
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ UnixUtilities.class);
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // getCommandLine //
+ //----------------//
+ /**
+ * Report the command line for the current process.
+ *
+ * @return the command line, or null if failed
+ */
+ public static String getCommandLine ()
+ {
+ try {
+ // Retrieve my command line
+ Path cmd = Paths.get("/proc/" + getPid() + "/cmdline");
+
+ if (Files.exists(cmd)) {
+ List lines = Files.readAllLines(
+ cmd,
+ StandardCharsets.US_ASCII);
+
+ if (!lines.isEmpty()) {
+ // BEWARE: spaces between arguments have been converted to 0
+ String cmdLine = lines.get(0)
+ .replace((char) 0, ' ')
+ .trim();
+ logger.debug("cmdline: {}", cmdLine);
+
+ return cmdLine;
+ }
+ }
+ } catch (IOException ex) {
+ logger.warn("Error in getCommandLine()", ex);
+ }
+
+ return null;
+ }
+
+ //--------//
+ // getPid //
+ //--------//
+ /**
+ * Report the pid of the current process.
+ *
+ * @return the process id, as a string
+ */
+ public static String getPid ()
+ throws IOException
+ {
+ String pid = new File("/proc/self").getCanonicalFile()
+ .getName();
+ logger.debug("pid: {}", pid);
+
+ return pid;
+ }
+}
diff --git a/src/installer/com/audiveris/installer/unix/VersionNumber.java b/src/installer/com/audiveris/installer/unix/VersionNumber.java
new file mode 100644
index 0000000..f9bd96b
--- /dev/null
+++ b/src/installer/com/audiveris/installer/unix/VersionNumber.java
@@ -0,0 +1,380 @@
+//----------------------------------------------------------------------------//
+// //
+// V e r s i o n N u m b e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Herve Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer.unix;
+
+import static com.audiveris.installer.RegexUtil.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class {@code VersionNumber} handles the version number of a Debian
+ * package, and especially version comparison.
+ *
+ * Based on
+ * http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
+ *
+ * Pattern is [epoch:]upstream_version[-debian_revision]
+ * epoch : integer
+ * upstream_version : [A-Za-z0-9.+-:~] starts with a digit
+ * debian_revision : [A-Za-z0-9.+~]
+ *
+ * @author Hervé Bitteur
+ */
+public class VersionNumber
+ implements Comparable
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ VersionNumber.class);
+
+ /** Regex for epoch. */
+ private static final Pattern epochPattern = Pattern.compile("^[0-9]+$");
+
+ /** Regex for version. */
+ private static final Pattern versionPattern = Pattern.compile(
+ "^[0-9][A-Za-z0-9\\.+-:~]*$");
+
+ /** Regex for revision. */
+ private static final Pattern revisionPattern = Pattern.compile(
+ "^[A-Za-z0-9\\.+~]+$");
+
+ /** Regex for parsing version (and revision) string. */
+ private static final String LETTERS = "letters";
+ private static final String DIGITS = "digits";
+ private static final String REM = "rem";
+ private static final Pattern lettersPattern = Pattern.compile(
+ "^" + group(LETTERS, "[^0-9]*") + group(REM, ".*") + "$");
+ private static final Pattern digitsPattern = Pattern.compile(
+ "^" + group(DIGITS, "[0-9]*") + group(REM, ".*") + "$");
+
+ //~ Instance fields --------------------------------------------------------
+
+ /** The original string (for debugging). */
+ private final String source;
+
+ /** The epoch part, if any. */
+ private final int epoch;
+
+ /** The upstream_version (mandatory). */
+ private final String version;
+
+ /** The debian_revision, if any. */
+ private final String revision;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //---------------//
+ // VersionNumber //
+ //---------------//
+ /**
+ * Creates a new VersionNumber object.
+ *
+ * @param source the source string to be parsed
+ */
+ public VersionNumber (String source)
+ {
+ this.source = source;
+
+ String src = source.trim();
+
+ // Epoch is a single (generally small) unsigned integer.
+ // It may be omitted, in which case zero is assumed.
+ // If it is omitted then the upstream_version may not contain any colons.
+ int colon = src.indexOf(":");
+
+ if (colon != -1) {
+ String epochStr = src.substring(0, colon);
+ checkEpoch(epochStr);
+ epoch = Integer.parseInt(epochStr);
+ src = src.substring(colon + 1);
+ } else {
+ epoch = 0;
+ }
+
+ // Break the version number apart at the last hyphen in the string
+ // (if there is one) to determine the upstream_version and debian_revision
+ int hyphen = src.lastIndexOf("-");
+
+ if (hyphen != -1) {
+ version = src.substring(0, hyphen);
+ checkVersion(version);
+ revision = src.substring(hyphen + 1);
+ checkRevision(revision);
+ } else {
+ version = src;
+ checkVersion(version);
+ revision = "0";
+ }
+
+ logger.debug("VersionNumber: {}", this);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //-----------//
+ // compareTo //
+ //-----------//
+ @Override
+ public int compareTo (VersionNumber that)
+ {
+ // 1/ epoch
+ int res = Integer.compare(this.epoch, that.epoch);
+
+ if (res != 0) {
+ return res;
+ }
+
+ // 2/ version
+ res = compareStrings(this.version, that.version);
+
+ if (res != 0) {
+ return res;
+ }
+
+ // 3/ revision
+ return compareStrings(this.revision, that.revision);
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder("{VersionNumber");
+
+ sb.append(" source=")
+ .append(source);
+ sb.append(" epoch=")
+ .append(epoch);
+ sb.append(" version=")
+ .append(version);
+ sb.append(" revision=")
+ .append(revision);
+
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ //------------//
+ // checkEpoch //
+ //------------//
+ private void checkEpoch (String str)
+ {
+ Matcher matcher = epochPattern.matcher(str);
+
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Illegal epoch string " + str);
+ }
+ }
+
+ //--------------//
+ // checkVersion //
+ //--------------//
+ private void checkRevision (String str)
+ {
+ Matcher matcher = revisionPattern.matcher(str);
+
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException(
+ "Illegal revision string " + str);
+ }
+ }
+
+ //--------------//
+ // checkVersion //
+ //--------------//
+ private void checkVersion (String str)
+ {
+ Matcher matcher = versionPattern.matcher(str);
+
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Illegal version string " + str);
+ }
+ }
+
+ //---------------//
+ // compareDigits //
+ //---------------//
+ private int compareDigits (String oneStr,
+ String twoStr)
+ {
+ /*
+ * The numerical values of
+ * these two parts are compared, and any difference found is returned as
+ * the result of the comparison. For these purposes an empty string
+ * (which can only occur at the end of one or both version strings
+ * being compared) counts as zero.
+ */
+ int one = oneStr.isEmpty() ? 0 : Integer.parseInt(oneStr);
+ int two = twoStr.isEmpty() ? 0 : Integer.parseInt(twoStr);
+
+ return Integer.compare(one, two);
+ }
+
+ //----------------//
+ // compareLetters //
+ //----------------//
+ private int compareLetters (String oneStr,
+ String twoStr)
+ {
+ /*
+ * The lexical comparison is a comparison of ASCII values
+ * modified so that all the letters sort earlier than all the
+ * non-letters and so that a tilde sorts before anything, even the end
+ * of a part. For example, the following parts are in sorted order from
+ * earliest to latest: ~~, ~~a, ~, the empty part, a.
+ */
+ final int max = Math.max(oneStr.length(), twoStr.length());
+ int res;
+
+ for (int i = 0; i < max; i++) {
+ // We code empty parts with spaces (which are not allowed in source)
+ char one = (i < oneStr.length()) ? oneStr.charAt(i) : ' ';
+ char two = (i < twoStr.length()) ? twoStr.charAt(i) : ' ';
+
+ // Tilde über alles (even empty part, which is coded as space)
+ if (one == '~') {
+ if (two != '~') {
+ return -1;
+ }
+ } else {
+ if (two == '~') {
+ return 1;
+ }
+ }
+
+ // Empty (space) before everything (except tilde)
+ if (one == ' ') {
+ return -1;
+ }
+
+ if (two == ' ') {
+ return 1;
+ }
+
+ // Letters before non-letters
+ if (Character.isLetter(one)) {
+ if (Character.isLetter(two)) {
+ res = one - two;
+
+ if (res != 0) {
+ return res;
+ }
+ } else {
+ return -1;
+ }
+ } else {
+ if (Character.isLetter(two)) {
+ return 1;
+ } else {
+ res = one - two;
+
+ if (res != 0) {
+ return res;
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ //----------------//
+ // compareStrings //
+ //----------------//
+ private int compareStrings (String one,
+ String two)
+ {
+ while (true) {
+ /*
+ * First the initial part of each string consisting entirely of
+ * non-digit characters is determined. These two parts (one of which
+ * may be empty) are compared lexically. If a difference is found it is
+ * returned.
+ */
+ final String oneLetters;
+ Matcher oneLettersMatcher = lettersPattern.matcher(one);
+
+ if (oneLettersMatcher.matches()) {
+ oneLetters = getGroup(oneLettersMatcher, LETTERS);
+ one = getGroup(oneLettersMatcher, REM);
+ } else {
+ oneLetters = "";
+ }
+
+ final String twoLetters;
+ Matcher twoLettersMatcher = lettersPattern.matcher(two);
+
+ if (twoLettersMatcher.matches()) {
+ twoLetters = getGroup(twoLettersMatcher, LETTERS);
+ two = getGroup(twoLettersMatcher, REM);
+ } else {
+ twoLetters = "";
+ }
+
+ int res = compareLetters(oneLetters, twoLetters);
+
+ if (res != 0) {
+ return res;
+ }
+
+ /*
+ * Then the initial part of the remainder of each string which consists
+ * entirely of digit characters is determined. The numerical values of
+ * these two parts are compared, and any difference found is returned as
+ * the result of the comparison.
+ */
+ final String oneDigits;
+ Matcher oneDigitsMatcher = digitsPattern.matcher(one);
+
+ if (oneDigitsMatcher.matches()) {
+ oneDigits = getGroup(oneDigitsMatcher, DIGITS);
+ one = getGroup(oneDigitsMatcher, REM);
+ } else {
+ oneDigits = "";
+ }
+
+ final String twoDigits;
+ Matcher twoDigitsMatcher = digitsPattern.matcher(two);
+
+ if (twoDigitsMatcher.matches()) {
+ twoDigits = getGroup(twoDigitsMatcher, DIGITS);
+ two = getGroup(twoDigitsMatcher, REM);
+ } else {
+ twoDigits = "";
+ }
+
+ res = compareDigits(oneDigits, twoDigits);
+
+ if (res != 0) {
+ return res;
+ }
+
+ /*
+ * These two steps (comparing and removing initial non-digit strings and
+ * initial digit strings) are repeated until a difference is found or
+ * both strings are exhausted.
+ */
+ if (one.isEmpty() && two.isEmpty()) {
+ return 0;
+ }
+ }
+ }
+}
diff --git a/src/installer/com/audiveris/installer/unix/package.html b/src/installer/com/audiveris/installer/unix/package.html
new file mode 100644
index 0000000..f450b84
--- /dev/null
+++ b/src/installer/com/audiveris/installer/unix/package.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Package com.audiveris.installer.unix
+
+
+
+
+ Specific implementations for Unix environment.
+
+
+
+
diff --git a/src/installer/com/audiveris/installer/windows/WindowsDescriptor.java b/src/installer/com/audiveris/installer/windows/WindowsDescriptor.java
new file mode 100644
index 0000000..19349f6
--- /dev/null
+++ b/src/installer/com/audiveris/installer/windows/WindowsDescriptor.java
@@ -0,0 +1,737 @@
+//----------------------------------------------------------------------------//
+// //
+// W i n d o w s D e s c r i p t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer.windows;
+
+import com.audiveris.installer.Descriptor;
+import com.audiveris.installer.DescriptorFactory;
+import com.audiveris.installer.Installer;
+import com.audiveris.installer.Jnlp;
+import static com.audiveris.installer.OcrCompanion.LOCAL_LIB_FOLDER;
+import static com.audiveris.installer.RegexUtil.*;
+import com.audiveris.installer.SpecificFile;
+import com.audiveris.installer.Utilities;
+
+import hudson.util.jna.Shell32;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.FileVisitResult;
+import static java.nio.file.FileVisitResult.CONTINUE;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class {@code WindowsDescriptor} implements Installer descriptor for
+ * Windows (32 and 64 bits).
+ *
+ * @author Hervé Bitteur
+ */
+public class WindowsDescriptor
+ implements Descriptor
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /**
+ * Usual logger utility
+ */
+ private static final Logger logger = LoggerFactory.getLogger(
+ WindowsDescriptor.class);
+
+ /**
+ * Specific prefix for application folders. {@value}
+ */
+ private static final String TOOL_PREFIX = "/" + COMPANY_ID + "/"
+ + TOOL_NAME;
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //-----------------//
+ // getConfigFolder //
+ //-----------------//
+ @Override
+ public File getConfigFolder ()
+ {
+ final String appdata = System.getenv("APPDATA");
+ final File root = new File(appdata + TOOL_PREFIX);
+ final File file = new File(root, "config");
+ logger.debug("getConfigFolder: {}", file.getAbsolutePath());
+
+ return file;
+ }
+
+ //----------------//
+ // getCopyCommand //
+ //----------------//
+ @Override
+ public String getCopyCommand (Path source,
+ Path target)
+ {
+ return "XCOPY \"" + source.toAbsolutePath() + "\" \""
+ + target.toAbsolutePath() + "\" /E /F /C /I /R /Y";
+ }
+
+ //---------------//
+ // getDataFolder //
+ //---------------//
+ @Override
+ public File getDataFolder ()
+ {
+ final String appdata = System.getenv("APPDATA");
+ final File root = new File(appdata + TOOL_PREFIX);
+ final File file = new File(root, "data");
+ logger.debug("getDataFolder: {}", file.getAbsolutePath());
+
+ return file;
+ }
+
+ //--------------------------//
+ // getDefaultTessdataPrefix //
+ //--------------------------//
+ @Override
+ public File getDefaultTessdataPrefix ()
+ {
+ final String pf32 = DescriptorFactory.OS_ARCH.equals("x86")
+ ? "ProgramFiles" : "ProgramFiles(x86)";
+ final String target = System.getenv(pf32);
+ final File file = new File(
+ new File(target),
+ Descriptor.TESSERACT_OCR);
+ logger.debug("getDefaultTessdataPrefix: {}", file.getAbsolutePath());
+
+ return file;
+ }
+
+ //------------------//
+ // getDeleteCommand //
+ //------------------//
+ @Override
+ public String getDeleteCommand (Path file)
+ {
+ return "DEL /F \"" + file.toAbsolutePath() + "\"";
+ }
+
+ //-----------------//
+ // getMkdirCommand //
+ //-----------------//
+ @Override
+ public String getMkdirCommand (Path dir)
+ {
+ return "mkdir \"" + dir.toAbsolutePath() + "\"";
+ }
+
+ //-------------------//
+ // getSetExecCommand //
+ //-------------------//
+ @Override
+ public String getSetExecCommand (Path file)
+ {
+ // This is void on Windows
+ return "";
+ }
+
+ //------------------//
+ // getSpecificFiles //
+ //------------------//
+ @Override
+ public List getSpecificFiles ()
+ {
+ final String appdata = System.getenv("APPDATA");
+ final File root = new File(appdata + TOOL_PREFIX);
+
+ return Arrays.asList(
+ new SpecificFile(
+ "windows/audiveris.bat",
+ root.getAbsolutePath() + "/audiveris.bat",
+ false));
+ }
+
+ //---------------//
+ // getTempFolder //
+ //---------------//
+ @Override
+ public File getTempFolder ()
+ {
+ final File folder = new File(getDataFolder(), "temp/installation");
+ logger.debug("getTempFolder: {}", folder.getAbsolutePath());
+
+ return folder;
+ }
+
+ //------------//
+ // installCpp //
+ //------------//
+ @Override
+ public void installCpp ()
+ throws Exception
+ {
+ final String url = DescriptorFactory.OS_ARCH.equals("x86") ? CPP.URL_32
+ : CPP.URL_64;
+ Utilities.downloadExecAndInstall(
+ "C++ runtime",
+ url,
+ getTempFolder(),
+ "/q");
+ }
+
+ //--------------------//
+ // installGhostscript //
+ //--------------------//
+ @Override
+ public void installGhostscript ()
+ throws Exception
+ {
+ final String url = DescriptorFactory.OS_ARCH.equals("x86") ? GS.URL_32
+ : GS.URL_64;
+ Utilities.downloadExecAndInstall(
+ "Ghostscript",
+ url,
+ getTempFolder(),
+ "/S");
+ }
+
+ //------------------//
+ // installTesseract //
+ //------------------//
+ @Override
+ public void installTesseract ()
+ throws Exception
+ {
+ /**
+ * The current strategy (for Windows) is to copy all needed
+ * DLLs into the proper target system folder.
+ * (The previous strategy was to keep them in Java cache, but we faced
+ * problems when trying to load them explicitly).
+ * As usual, we download and expand locally, then try to copy the
+ * expanded files to target system folder.
+ * If direct copy is denied, we post copy commands to be run later at
+ * elevated level.
+ */
+ final File local = getLocalLibFolder();
+ final File lib = new File(local, "lib");
+ lib.mkdirs();
+ logger.debug("local lib folder: {}", lib.getAbsolutePath());
+
+ final URI codeBase = Jnlp.basicService.getCodeBase()
+ .toURI();
+ final String jarName = DescriptorFactory.OS_ARCH.equals("x86")
+ ? TESS.JAR_32 : TESS.JAR_64;
+ final URL url = Utilities.toURI(codeBase, jarName)
+ .toURL();
+ Utilities.downloadJarAndExpand(
+ "Tesseract",
+ url.toString(),
+ getTempFolder(),
+ "",
+ lib);
+
+ // (Try to) copy each file from local lib folder to system target folder
+ final Path libPath = lib.toPath();
+ final String sysDir = DescriptorFactory.WOW ? TESS.SYSTEM_WOW
+ : TESS.SYSTEM_PURE;
+ final Path target = Paths.get(sysDir);
+
+ Files.walkFileTree(
+ libPath,
+ new SimpleFileVisitor()
+ {
+ @Override
+ public FileVisitResult visitFile (Path file,
+ BasicFileAttributes attrs)
+ throws IOException
+ {
+ Path dest = target.resolve(file.getFileName());
+
+ try {
+ // Try immediate copy
+ Files.copy(file, dest, REPLACE_EXISTING);
+ logger.info(
+ "Copied file {} to file {}",
+ file,
+ dest);
+ } catch (IOException ex) {
+ // Fallback to posted copy commands
+ Installer.getBundle()
+ .appendCommand(getCopyCommand(file, target));
+ }
+
+ return CONTINUE;
+ }
+ });
+ }
+
+ //---------//
+ // isAdmin //
+ //---------//
+ @Override
+ public boolean isAdmin ()
+ {
+ // The UAC (User Access Control) appeared with Windows Vista
+ // Before that, user was granted admin privileges by default
+ try {
+ // If the IsUserAnAdmin method exists, then we are in Vista or later
+ // and just need to check its result
+ return Shell32.INSTANCE.IsUserAnAdmin();
+ } catch (Throwable ex) {
+ // No access to IsUserAnAdmin, so we assume there is no UAC
+ return true;
+ }
+ }
+
+ //----------------//
+ // isCppInstalled //
+ //----------------//
+ @Override
+ public boolean isCppInstalled ()
+ {
+ List output = new ArrayList();
+
+ try {
+ // Check Windows registry
+ final String radix = DescriptorFactory.WOW ? CPP.RADIX_WOW
+ : CPP.RADIX_PURE;
+ final String key = radix
+ + (DescriptorFactory.OS_ARCH.equals("x86")
+ ? CPP.KEY_32 : CPP.KEY_64);
+ int result = WindowsUtilities.queryRegistry(
+ output,
+ key,
+ "/v",
+ CPP.VALUE);
+ logger.debug("C++ query exit:{} output: {}", result, output);
+
+ return result == 0;
+ } catch (Exception ex) {
+ logger.warn(Utilities.dumpOfLines(output));
+
+ return false;
+ }
+ }
+
+ //------------------------//
+ // isGhostscriptInstalled //
+ //------------------------//
+ @Override
+ public boolean isGhostscriptInstalled ()
+ {
+ return getGhostscriptPath() != null;
+ }
+
+ //----------------------//
+ // isTesseractInstalled //
+ //----------------------//
+ @Override
+ public boolean isTesseractInstalled ()
+ {
+ final String sysDir = DescriptorFactory.WOW ? TESS.SYSTEM_WOW
+ : TESS.SYSTEM_PURE;
+
+ return Files.exists(Paths.get(sysDir, TESS.DLL_LEPTONICA))
+ && Files.exists(Paths.get(sysDir, TESS.DLL_TESSERACT))
+ && Files.exists(Paths.get(sysDir, TESS.DLL_BRIDGE));
+ }
+
+ //----------//
+ // runShell //
+ //----------//
+ @Override
+ public boolean runShell (boolean asAdmin,
+ List commands)
+ throws Exception
+ {
+ // Build a single compound command
+ StringBuilder sb = new StringBuilder();
+
+ for (String command : commands) {
+ if (sb.length() > 0) {
+ sb.append(" && ");
+ }
+
+ sb.append(command);
+ }
+
+ final String cmdLine = sb.toString();
+
+ if (asAdmin) {
+ final String cmdExe = System.getenv("ComSpec");
+ WindowsUtilities.runElevated(
+ new File(cmdExe),
+ new File("."),
+ "/S",
+ "/E:ON",
+ "/C",
+ cmdLine);
+ } else {
+ List output = new ArrayList();
+ int res = Utilities.runProcess(
+ output,
+ "cmd.exe",
+ "/c",
+ cmdLine);
+
+ if (res != 0) {
+ final String lines = Utilities.dumpOfLines(output);
+ logger.warn(lines);
+ throw new RuntimeException("Failure in runShell().\n" + lines);
+ }
+ }
+
+ return true;
+ }
+
+ // //--------//
+ // // setenv //
+ // //--------//
+ // @Override
+ // public void setenv (boolean system,
+ // String var,
+ // String value)
+ // throws IOException, InterruptedException
+ // {
+ //// List args = new ArrayList<>();
+ //// args.add("/C");
+ //// args.add("setx");
+ //// args.add(var);
+ //// args.add(value);
+ //// if (system) {
+ //// args.add("/M");
+ //// }
+ //// List output = new ArrayList<>();
+ //// Utilities.runProcess("cmd.exe", output, args.toArray(new String[args.size()]));
+ //// logger.debug("setenv output: {}", output);
+ // final List output = new ArrayList<>();
+ // final String key = system ? ENV.SYSTEM_KEY : ENV.USER_KEY;
+ // WindowsUtilities.setRegistry(output, key, "/v", var, "/d", value);
+ // logger.debug("setenv output: {}", output);
+ // }
+ //---------------//
+ // setExecutable //
+ //---------------//
+ @Override
+ public void setExecutable (Path file)
+ throws Exception
+ {
+ // Void on Windows
+ }
+
+ //--------------------//
+ // getGhostscriptPath //
+ //--------------------//
+ /**
+ * Retrieve the path to suitable ghostscript executable on Windows
+ * environments.
+ *
+ * This is implemented on registry informations, using CLI "reg" command:
+ * reg query "HKLM\SOFTWARE\GPL Ghostscript" /s
+ *
+ * @return the best suitable path, or null if nothing found
+ */
+ private String getGhostscriptPath ()
+ {
+ // Group names
+ final String VERSION = "version";
+ final String PATH = "path";
+ final String ARCH = "arch";
+
+ /**
+ * Regex for registry key line.
+ */
+ final Pattern keyPattern = Pattern.compile(
+ "^HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\(Wow6432Node\\\\)?GPL Ghostscript\\\\"
+ + group(VERSION, "\\d+\\.\\d+") + "$");
+
+ /**
+ * Regex for registry value line.
+ */
+ final Pattern valPattern = Pattern.compile(
+ "^\\s+GS_DLL\\s+REG_SZ\\s+" + group(PATH, ".+") + "$");
+
+ /**
+ * Regex for dll name.
+ */
+ final Pattern dllPattern = Pattern.compile(
+ "gsdll" + group(ARCH, "\\d+") + "\\.dll$");
+
+ Double bestVersion = null; // Best version found so far
+ String bestPath = null; // Best path found so far
+ boolean relevant = false; // Is current registry info interesting?
+ int index = 0; // Line number in registry outputs
+
+ double minVersion = Double.valueOf(
+ Descriptor.GHOSTSCRIPT_MIN_VERSION);
+
+ // Browse registry output lines in sequence
+ for (String line : getRegistryGhostscriptOutputs()) {
+ logger.debug("Line#{}:{}", ++index, line);
+
+ Matcher keyMatcher = keyPattern.matcher(line);
+
+ if (keyMatcher.matches()) {
+ relevant = false;
+
+ // Check version information
+ String versionStr = getGroup(keyMatcher, VERSION);
+ logger.debug("Version read as: {}", versionStr);
+
+ Double version = Double.valueOf(versionStr);
+
+ if ((version != null) && (version >= minVersion)) {
+ // We have an acceptable version
+ if ((bestVersion == null) || (bestVersion < version)) {
+ bestVersion = version;
+ logger.debug("Best version is: {}", versionStr);
+ relevant = true;
+ } else {
+ logger.debug("Version discarded: {}", versionStr);
+ }
+ } else {
+ logger.debug("Version unacceptable: {}", versionStr);
+ }
+ } else if (relevant) {
+ Matcher valMatcher = valPattern.matcher(line);
+
+ if (valMatcher.matches()) {
+ // Read path information
+ bestPath = getGroup(valMatcher, PATH);
+ logger.debug("Best path is: {}", bestPath);
+ }
+ }
+ }
+
+ // Extract prefix and dll from best path found, regardless of arch
+ if (bestPath != null) {
+ int lastSep = bestPath.lastIndexOf("\\");
+ String prefix = bestPath.substring(0, lastSep);
+ logger.debug("Prefix is: {}", prefix);
+
+ String dll = bestPath.substring(lastSep + 1);
+ logger.debug("Dll is: {}", dll);
+
+ Matcher dllMatcher = dllPattern.matcher(dll);
+
+ if (dllMatcher.matches()) {
+ String arch = getGroup(dllMatcher, ARCH);
+ String result = prefix + "\\gswin" + arch + "c.exe";
+ logger.debug("Final path is: {}", result);
+
+ return result; // Normal exit
+ }
+ }
+
+ logger.info("Could not find suitable Ghostscript software");
+
+ return null; // Abnormal exit
+ }
+
+ //-------------------//
+ // getLocalLibFolder //
+ //-------------------//
+ /**
+ * Report the local (temporary) folder where binary files are
+ * expanded before being copied to final target location.
+ *
+ * @return the local lib folder
+ */
+ private File getLocalLibFolder ()
+ {
+ return new File(getTempFolder(), LOCAL_LIB_FOLDER);
+ }
+
+ //-------------------------------//
+ // getRegistryGhostscriptOutputs //
+ //-------------------------------//
+ /**
+ * Collect the output lines from registry queries about Ghostscript
+ *
+ * @return the output lines
+ */
+ private List getRegistryGhostscriptOutputs ()
+ {
+ /**
+ * Radices used in registry search (32, 64 or Wow).
+ */
+ final String[] radices = new String[]{
+ GS.RADIX_PURE, // Pure 32/32 or 64/64
+ GS.RADIX_WOW // Wow (64/32)
+ };
+
+ // Access registry twice, one for win32 & win64 and one for Wow
+ List outputs = new ArrayList();
+
+ for (String radix : radices) {
+ logger.debug("Radix: {}", radix);
+
+ try {
+ WindowsUtilities.queryRegistry(
+ outputs,
+ radix,
+ "/s");
+ } catch (Exception ex) {
+ logger.error("Error in reading registry", ex);
+ }
+ }
+
+ return outputs;
+ }
+
+ //~ Inner Interfaces -------------------------------------------------------
+ /**
+ * Data for Microsoft Visual C++ 2008 Redistributable.
+ */
+ private static interface CPP
+ {
+ //~ Static fields/initializers -----------------------------------------
+
+ /**
+ * Registry value name.
+ */
+ static final String VALUE = "DisplayName";
+
+ /**
+ * Registry radix for Wow (32/64).
+ */
+ static final String RADIX_WOW = "HKLM\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";
+
+ /**
+ * Registry radix for pure 32/32 or 64/64.
+ */
+ static final String RADIX_PURE = "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";
+
+ /**
+ * Registry key for 32-bit.
+ */
+ static final String KEY_32 = "{9BE518E6-ECC6-35A9-88E4-87755C07200F}";
+
+ /**
+ * Registry key for 64-bit.
+ */
+ static final String KEY_64 = "{5FCE6D76-F5DC-37AB-B2B8-22AB8CEDB1D4}";
+
+ /**
+ * Download URL for 32-bit.
+ */
+ static final String URL_32 = "http://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe";
+
+ /**
+ * Download URL for 64-bit.
+ */
+ static final String URL_64 = "http://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe";
+
+ }
+
+ /**
+ * For environment variables.
+ */
+ private static interface ENV
+ {
+ //~ Static fields/initializers -----------------------------------------
+
+ /**
+ * Registry key for machine environment variable.
+ */
+ static final String SYSTEM_KEY = "\"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\"";
+
+ /**
+ * Registry key for user environment variable.
+ */
+ static final String USER_KEY = "HKCU\\Environment";
+
+ }
+
+ /**
+ * Data for Ghostscript.
+ */
+ private static interface GS
+ {
+ //~ Static fields/initializers -----------------------------------------
+
+ /**
+ * Registry radix for pure 32/32 or 64/64.
+ */
+ static final String RADIX_PURE = "HKLM\\SOFTWARE\\GPL Ghostscript";
+
+ /**
+ * Registry radix for Wow (32/64).
+ */
+ static final String RADIX_WOW = "HKLM\\SOFTWARE\\Wow6432Node\\GPL Ghostscript";
+
+ /**
+ * Download URL for 32-bit.
+ */
+ static final String URL_32 = "http://downloads.ghostscript.com/public/gs907w32.exe";
+
+ /**
+ * Download URL for 64-bit.
+ */
+ static final String URL_64 = "http://downloads.ghostscript.com/public/gs907w64.exe";
+
+ }
+
+ /**
+ * Data for Tesseract.
+ */
+ private static interface TESS
+ {
+ //~ Static fields/initializers -----------------------------------------
+
+ /**
+ * System location for pure 32/32 or 64/64.
+ */
+ static final String SYSTEM_PURE = System.getenv("SystemRoot")
+ + "\\System32";
+
+ /**
+ * System location for Wow (32/64).
+ */
+ static final String SYSTEM_WOW = System.getenv("SystemRoot")
+ + "\\SysWow64";
+
+ /**
+ * Jar name for 32-bit.
+ */
+ static final String JAR_32 = "resources/tess-windows-32bit.jar";
+
+ /**
+ * Jar name for 64-bit.
+ */
+ static final String JAR_64 = "resources/tess-windows-64bit.jar";
+
+ /**
+ * Dll for leptonica.
+ */
+ static final String DLL_LEPTONICA = "liblept168.dll";
+
+ /**
+ * Dll for tesseract.
+ */
+ static final String DLL_TESSERACT = "libtesseract302.dll";
+
+ /**
+ * Dll for bridge.
+ */
+ static final String DLL_BRIDGE = "jniTessBridge.dll";
+
+ }
+}
diff --git a/src/installer/com/audiveris/installer/windows/WindowsUtilities.java b/src/installer/com/audiveris/installer/windows/WindowsUtilities.java
new file mode 100644
index 0000000..9cefaf2
--- /dev/null
+++ b/src/installer/com/audiveris/installer/windows/WindowsUtilities.java
@@ -0,0 +1,157 @@
+//----------------------------------------------------------------------------//
+// //
+// W i n d o w s U t i l i t i e s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package com.audiveris.installer.windows;
+
+import com.audiveris.installer.Utilities;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+
+import hudson.util.jna.Kernel32;
+import hudson.util.jna.Kernel32Utils;
+import hudson.util.jna.SHELLEXECUTEINFO;
+import hudson.util.jna.Shell32;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class {@code WindowsUtilities} gathers utilities that are relevant
+ * for the Windows platform only.
+ *
+ * @author Hervé Bitteur
+ */
+public class WindowsUtilities
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ WindowsUtilities.class);
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // getCommandLine //
+ //----------------//
+ public static String getCommandLine ()
+ {
+ return Kernel32.INSTANCE.GetCommandLineW()
+ .toString();
+ }
+
+ //-------------------//
+ // getModuleFilename //
+ //-------------------//
+ public static String getModuleFilename ()
+ {
+ final int MAX_SIZE = 512;
+ byte[] exePathname = new byte[MAX_SIZE];
+ Pointer zero = new Pointer(0);
+ int result = Kernel32.INSTANCE.GetModuleFileNameA(
+ zero,
+ exePathname,
+ MAX_SIZE);
+
+ return Native.toString(exePathname);
+ }
+
+ //-------------//
+ // runElevated //
+ //-------------//
+ public static int runElevated (File exec,
+ File pwd,
+ String... args)
+ throws IOException, InterruptedException
+ {
+ // Concatenate arguments into one string
+ StringBuilder sb = new StringBuilder();
+
+ for (String arg : args) {
+ sb.append(arg)
+ .append(" ");
+ }
+
+ logger.debug("exec: {}", exec);
+ logger.debug("pwd: {}", pwd);
+ logger.debug("params: {}", sb);
+ logger.debug("Calling ShellExecuteEx...");
+
+ // error code 740 is ERROR_ELEVATION_REQUIRED, indicating that
+ // we run in UAC-enabled Windows and we need to run this in an elevated privilege
+ SHELLEXECUTEINFO sei = new SHELLEXECUTEINFO();
+ sei.fMask = SHELLEXECUTEINFO.SEE_MASK_NOCLOSEPROCESS;
+ sei.lpVerb = "runas";
+ sei.lpFile = exec.getAbsolutePath();
+ sei.lpParameters = sb.toString();
+ sei.lpDirectory = pwd.getAbsolutePath();
+ sei.nShow = SHELLEXECUTEINFO.SW_HIDE;
+
+ if (!Shell32.INSTANCE.ShellExecuteEx(sei)) {
+ throw new IOException(
+ "Failed to shellExecute: " + Native.getLastError());
+ }
+
+ try {
+ int result = Kernel32Utils.waitForExitProcess(sei.hProcess);
+ logger.debug("result: {}", result);
+
+ return result;
+ } finally {
+ // TODO: need to print content of stdout/stderr
+ // FileInputStream fin = new FileInputStream(
+ // new File(pwd, "redirect.log"));
+ // IOUtils.copy(fin, out.getLogger());
+ // fin.close();
+ }
+ }
+
+ //---------------//
+ // queryRegistry //
+ //---------------//
+ public static int queryRegistry (List output,
+ String... args)
+ throws IOException, InterruptedException
+ {
+ // Command arguments
+ List cmdArgs = new ArrayList<>();
+ cmdArgs.add("cmd.exe");
+ cmdArgs.addAll(Arrays.asList("/c", "reg", "query"));
+ cmdArgs.addAll(Arrays.asList(args));
+ logger.debug("cmd query: {}", cmdArgs);
+
+ return Utilities.runProcess(output,
+ cmdArgs.toArray(new String[cmdArgs.size()]));
+ }
+
+ //-------------//
+ // setRegistry //
+ //-------------//
+ public static int setRegistry (List output,
+ String... args)
+ throws IOException, InterruptedException
+ {
+ // Command arguments
+ List cmdArgs = new ArrayList<>();
+ cmdArgs.add("cmd.exe");
+ cmdArgs.addAll(Arrays.asList("/c", "reg", "add"));
+ cmdArgs.addAll(Arrays.asList(args));
+ logger.debug("cmd add: {}", cmdArgs);
+
+ return Utilities.runProcess(output,
+ cmdArgs.toArray(new String[cmdArgs.size()]));
+ }
+}
diff --git a/src/installer/com/audiveris/installer/windows/package.html b/src/installer/com/audiveris/installer/windows/package.html
new file mode 100644
index 0000000..9a25208
--- /dev/null
+++ b/src/installer/com/audiveris/installer/windows/package.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Package com.audiveris.installer.windows
+
+
+
+
+ Specific implementations for Windows environment.
+
+
+
+
diff --git a/src/installer/doc-files/overview.uxf b/src/installer/doc-files/overview.uxf
new file mode 100644
index 0000000..4afbb7b
--- /dev/null
+++ b/src/installer/doc-files/overview.uxf
@@ -0,0 +1,499 @@
+
+
+ 10
+
+ com.umlet.element.Class
+
+ 10
+ 330
+ 130
+ 160
+
+ <<factory>>
+*DescriptorFactory*
+bg=#888888
+--
+OS_NAME
+OS_ARCH
+LINUX
+MAC_OS_X
+WINDOWS
+--
+getDescriptor()
+
+
+
+ com.umlet.element.Class
+
+ 180
+ 420
+ 80
+ 50
+
+ <<interface>>
+bg=#bbbbbb
+*Descriptor*
+
+
+
+ com.umlet.element.Relation
+
+ 620
+ 330
+ 50
+ 250
+
+ lt=<<-
+ 30;30;30;230
+
+
+ com.umlet.element.Relation
+
+ 510
+ 90
+ 110
+ 50
+
+ lt=<<<<->
+ 30;30;90;30
+
+
+ com.umlet.element.Class
+
+ 10
+ 10
+ 200
+ 260
+
+ <<facade>>
+*Jnlp*
+bg=#ffeedd
+--
+*basicService:*
+--
++getCodebase()
++isWebBrowserSupported()
++showDocument()
+--
+*extensionInstallerService:*
+--
++setHeading()
++setStatus()
++updateProgress()
++installSucceeded()
++installFailed()
+--
+*downloadService:*
+(unused)
+
+
+
+ com.umlet.element.Class
+
+ 600
+ 100
+ 120
+ 120
+
+ <<interface>>
+*Companion*
+bg=#aaffaa
+--
+--
+isNeeded()
+checkInstalled()
+install()
+uninstall()
+
+
+
+ com.umlet.element.Class
+
+ 390
+ 400
+ 150
+ 30
+
+ LicenseCompanion
+
+
+
+ com.umlet.element.Class
+
+ 510
+ 560
+ 150
+ 30
+
+ DocCompanion
+
+
+
+ com.umlet.element.Relation
+
+ 20
+ 550
+ 120
+ 50
+
+ lt=-
+ 30;30;100;30
+
+
+ com.umlet.element.Class
+
+ 80
+ 530
+ 130
+ 30
+
+ WindowsDescriptor
+
+
+
+ com.umlet.element.Class
+
+ 600
+ 10
+ 120
+ 50
+
+ *CompanionView*
+bg=#eeeeff
+--
+--
+update()
+
+
+
+ com.umlet.element.Relation
+
+ 630
+ 30
+ 50
+ 90
+
+ lt=-
+ 30;30;30;70
+
+
+ com.umlet.element.Class
+
+ 520
+ 260
+ 230
+ 100
+
+ /AbstractCompanion/
+--
+--
+/#doInstall()/
+#doUninstall()
+#getTargetFolder()
+#makeTargetFolder()
+
+
+
+ com.umlet.element.Relation
+
+ 210
+ 440
+ 50
+ 190
+
+ lt=<<.
+ 30;30;30;170
+
+
+ com.umlet.element.Relation
+
+ 590
+ 330
+ 50
+ 210
+
+ lt=<<-
+ 30;30;30;190
+
+
+ com.umlet.element.Class
+
+ 120
+ 570
+ 110
+ 30
+
+ UnixDescriptor
+
+
+
+ com.umlet.element.Relation
+
+ 650
+ 330
+ 50
+ 290
+
+ lt=<<-
+ 30;30;30;270
+
+
+ com.umlet.element.Relation
+
+ 170
+ 440
+ 50
+ 110
+
+ lt=<<.
+ 30;30;30;90
+
+
+ com.umlet.element.Class
+
+ 420
+ 440
+ 150
+ 30
+
+ CppCompanion
+
+
+
+ com.umlet.element.Relation
+
+ 630
+ 190
+ 50
+ 90
+
+ lt=<<.
+ 30;30;30;70
+
+
+ com.umlet.element.Relation
+
+ 320
+ 90
+ 110
+ 50
+
+ lt=-
+ 30;30;90;30
+
+
+ com.umlet.element.Class
+
+ 410
+ 10
+ 130
+ 50
+
+ *BundleView*
+bg=#eeeeff
+--
+--
+publishMessage()
+
+
+
+ com.umlet.element.Relation
+
+ 20
+ 460
+ 140
+ 180
+
+ lt=<<<<-
+ 30;30;30;160;120;160
+
+
+ com.umlet.element.Class
+
+ 600
+ 680
+ 150
+ 30
+
+ TrainingCompanion
+
+
+
+ com.umlet.element.Class
+
+ 410
+ 100
+ 130
+ 70
+
+ *Bundle*
+bg=#ffffcc
+--
+--
+installBundle()
+uninstallBundle()
+
+
+
+ com.umlet.element.Relation
+
+ 680
+ 330
+ 50
+ 330
+
+ lt=<<-
+ 30;30;30;310
+
+
+ com.umlet.element.Class
+
+ 540
+ 600
+ 150
+ 30
+
+ ExamplesCompanion
+
+
+
+ com.umlet.element.Class
+
+ 140
+ 610
+ 110
+ 30
+
+ MacDescriptor
+
+
+
+ com.umlet.element.Relation
+
+ 560
+ 330
+ 50
+ 170
+
+ lt=<<-
+ 30;30;30;150
+
+
+ com.umlet.element.Relation
+
+ 190
+ 440
+ 50
+ 150
+
+ lt=<<.
+ 30;30;30;130
+
+
+ com.umlet.element.Class
+
+ 570
+ 640
+ 150
+ 30
+
+ PluginsCompanion
+
+
+
+ com.umlet.element.Class
+
+ 480
+ 520
+ 150
+ 30
+
+ OcrCompanion
+
+
+
+ com.umlet.element.Relation
+
+ 710
+ 330
+ 50
+ 370
+
+ lt=<<-
+ 30;30;30;350
+
+
+ com.umlet.element.Relation
+
+ 450
+ 30
+ 50
+ 90
+
+ lt=-
+ 30;30;30;70
+
+
+ com.umlet.element.Relation
+
+ 500
+ 330
+ 50
+ 90
+
+ lt=<<-
+ 30;30;30;70
+
+
+ com.umlet.element.Relation
+
+ 20
+ 510
+ 80
+ 50
+
+ lt=-
+ 30;30;60;30
+
+
+ com.umlet.element.Relation
+
+ 530
+ 330
+ 50
+ 130
+
+ lt=<<-
+ 30;30;30;110
+
+
+ com.umlet.element.Class
+
+ 450
+ 480
+ 150
+ 30
+
+ GhostscriptCompanion
+
+
+
+ com.umlet.element.Class
+
+ 250
+ 100
+ 100
+ 120
+
+ *Installer*
+bg=#ffaaaa
+--
+--
+getBundle()
+getFrame()
+main()
+install()
+uninstall()
+
+
+
diff --git a/src/installer/doc-files/roles.uxf b/src/installer/doc-files/roles.uxf
new file mode 100644
index 0000000..11f7b59
--- /dev/null
+++ b/src/installer/doc-files/roles.uxf
@@ -0,0 +1,585 @@
+
+
+ 10
+
+ com.umlet.element.custom.Database
+
+ 480
+ 270
+ 110
+ 40
+
+ launch.jnlp
+
+
+
+ com.umlet.element.Class
+
+ 860
+ 160
+ 200
+ 30
+
+ *javaws.exe (64)*
+bg=#88ff88
+
+
+
+ com.umlet.element.Class
+
+ 10
+ 160
+ 240
+ 30
+
+ *javaws.exe (32)*
+bg=#ff8888
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 350
+ 110
+ 40
+
+ audiveris.jar
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 430
+ 110
+ 40
+
+ ...
+
+
+
+ com.umlet.element.custom.Database
+
+ 360
+ 630
+ 160
+ 100
+
+ *tess-windows-32bit.jar*
+bg=#ff8888
+--
+jniTessBridge.dll
+libtesseract302.dll
+liblept168.dll
+fg=#ff8888
+
+
+
+ com.umlet.element.custom.Database
+
+ 470
+ 740
+ 130
+ 40
+
+ documentation.jar
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 780
+ 110
+ 40
+
+ train.jar
+
+
+
+ com.umlet.element.Relation
+
+ 220
+ 120
+ 300
+ 70
+
+ lt=<-
+?
+ 30;50;280;50
+
+
+ com.umlet.element.Relation
+
+ 540
+ 120
+ 340
+ 70
+
+ lt=->
+?
+ 30;50;320;50
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 820
+ 110
+ 40
+
+ examples.jar
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 860
+ 110
+ 40
+
+ specifics.jar
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 900
+ 110
+ 40
+
+ plugins.jar
+
+
+
+ com.umlet.element.Class
+
+ 740
+ 1020
+ 320
+ 30
+
+ C++ runtime (64)
+bg=#88ff88
+
+
+
+ com.umlet.element.Class
+
+ 10
+ 1020
+ 320
+ 30
+
+ C++ runtime (32)
+bg=#ff8888
+
+
+
+ com.umlet.element.Class
+
+ 10
+ 660
+ 170
+ 60
+
+ jniTessBridge.dll (32)
+libtesseract302.dll (32)
+liblept168.dll (32)
+bg=#ff8888
+
+
+
+ com.umlet.element.Class
+
+ 900
+ 660
+ 160
+ 60
+
+ jniTessBridge.dll (64)
+libtesseract302.dll (64)
+liblept168.dll (64)
+bg=#88ff88
+
+
+
+ com.umlet.element.custom.Database
+
+ 550
+ 630
+ 160
+ 100
+
+ *tess-windows-64bit.jar*
+--
+jniTessBridge.dll
+libtesseract302.dll
+liblept168.dll
+fg=#88ff88
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 510
+ 110
+ 40
+
+ installer.jar
+
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 470
+ 110
+ 40
+
+ tesseract-3.jar
+
+
+
+ com.umlet.element.Package
+
+ 350
+ 600
+ 370
+ 350
+
+ resources (for Installer)
+bg=#aaffff
+
+
+
+ com.umlet.element.Package
+
+ 460
+ 400
+ 150
+ 160
+
+ lib
+
+
+
+ com.umlet.element.Package
+
+ 390
+ 1090
+ 290
+ 160
+
+ Google site
+bg=#cccccc
+
+
+
+ com.umlet.element.custom.Database
+
+ 440
+ 1120
+ 190
+ 40
+
+ tesseract-ocr-3.02.eng.tar.gz
+
+
+
+ com.umlet.element.custom.Database
+
+ 440
+ 1160
+ 190
+ 40
+
+ tesseract-ocr-3.02.ita.tar.gz
+
+
+
+ com.umlet.element.custom.Database
+
+ 440
+ 1200
+ 190
+ 40
+
+ ...
+
+
+
+ com.umlet.element.Class
+
+ 860
+ 190
+ 200
+ 30
+
+ c:\Program Files\Java\jre7\bin\
+
+
+
+ com.umlet.element.Class
+
+ 740
+ 1050
+ 320
+ 30
+
+ Microsoft Visual C++ 2008 Redistributable x64 9.0
+
+
+
+ com.umlet.element.Class
+
+ 10
+ 1050
+ 320
+ 30
+
+ Microsoft Visual C++ 2008 Redistributable x86 9.0
+
+
+
+ UMLClass
+
+ 900
+ 720
+ 160
+ 30
+
+ c:\Windows\System32\
+valign=center
+
+
+
+ UMLClass
+
+ 10
+ 720
+ 170
+ 30
+
+ c:\Windows\SysWOW64\
+valign=center
+
+
+
+ com.umlet.element.Class
+
+ 10
+ 190
+ 240
+ 30
+
+ c:\Program Files (x86)\Java\jre7\bin\
+
+
+
+ com.umlet.element.Relation
+
+ 680
+ 610
+ 240
+ 70
+
+ lt=-
+copy>
+ 30;50;220;50
+
+
+ com.umlet.element.Relation
+
+ 150
+ 610
+ 230
+ 70
+
+ lt=-
+<copy
+ 30;50;210;50
+
+
+ com.umlet.element.Package
+
+ 390
+ 980
+ 290
+ 80
+
+ Microsoft site
+bg=#cccccc
+
+
+
+ com.umlet.element.custom.Database
+
+ 400
+ 1010
+ 120
+ 40
+
+ *vcredist_x86.exe*
+fg=#ff8888
+
+
+
+ com.umlet.element.custom.Database
+
+ 550
+ 1010
+ 120
+ 40
+
+ *vcredist_x64.exe*
+fg=#88ff88
+
+
+
+ com.umlet.element.Relation
+
+ 640
+ 980
+ 120
+ 70
+
+ lt=-
+install>
+ 30;50;100;50
+
+
+ com.umlet.element.Relation
+
+ 300
+ 980
+ 120
+ 70
+
+ lt=-
+<install
+ 30;50;100;50
+
+
+ com.umlet.element.custom.Database
+
+ 480
+ 310
+ 110
+ 40
+
+ installer.jnlp
+
+
+
+ com.umlet.element.Package
+
+ 440
+ 240
+ 190
+ 330
+
+ kept in java cache
+bg=#ffffaa
+
+
+
+ UMLActor
+
+ 500
+ 10
+ 60
+ 100
+
+ user
+bg=red
+
+
+
+ com.umlet.element.custom.State
+
+ 480
+ 110
+ 110
+ 40
+
+ web browser
+bg=#8888ff
+
+
+
+ com.umlet.element.Relation
+
+ 220
+ 100
+ 280
+ 90
+
+ lt=<-
+?
+ 30;70;260;30
+
+
+ com.umlet.element.Relation
+
+ 560
+ 100
+ 320
+ 90
+
+ lt=->
+?
+ 30;30;300;70
+
+
+ com.umlet.element.custom.State
+
+ 500
+ 160
+ 70
+ 20
+
+ shortcut
+bg=#ff0000
+
+
+
+ com.umlet.element.custom.State
+
+ 480
+ 190
+ 110
+ 20
+
+ command line
+
+
+
+ com.umlet.element.Relation
+
+ 560
+ 140
+ 320
+ 80
+
+ lt=->
+?
+ 30;60;300;30
+
+
+ com.umlet.element.Relation
+
+ 220
+ 140
+ 280
+ 80
+
+ lt=<-
+?
+ 30;30;260;60
+
+
diff --git a/src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java b/src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java
new file mode 100644
index 0000000..ea48d46
--- /dev/null
+++ b/src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java
@@ -0,0 +1,58 @@
+
+package hudson.util.jna;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * {@link InvocationHandler} that reports the same exception over and over again when methods are invoked
+ * on the interface.
+ *
+ * This is convenient to remember why the initialization of the real JNA proxy failed.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.487
+ * @see Related bug report against JDK
+ */
+public class InitializationErrorInvocationHandler
+ implements InvocationHandler
+{
+ //~ Instance fields --------------------------------------------------------
+
+ private final Throwable cause;
+
+ //~ Constructors -----------------------------------------------------------
+
+ private InitializationErrorInvocationHandler (Throwable cause)
+ {
+ this.cause = cause;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ public static T create (Class type,
+ Throwable cause)
+ {
+ return type.cast(
+ Proxy.newProxyInstance(
+ type.getClassLoader(),
+ new Class[] { type },
+ new InitializationErrorInvocationHandler(cause)));
+ }
+
+ @Override
+ public Object invoke (Object proxy,
+ Method method,
+ Object[] args)
+ throws Throwable
+ {
+ if (method.getDeclaringClass() == Object.class) {
+ return method.invoke(this, args);
+ }
+
+ throw new UnsupportedOperationException(
+ "Failed to link the library: " + method.getDeclaringClass(),
+ cause);
+ }
+}
diff --git a/src/installer/hudson/util/jna/Kernel32.java b/src/installer/hudson/util/jna/Kernel32.java
new file mode 100644
index 0000000..80e2f44
--- /dev/null
+++ b/src/installer/hudson/util/jna/Kernel32.java
@@ -0,0 +1,93 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.util.jna;
+
+import com.sun.jna.Pointer;
+import com.sun.jna.WString;
+import com.sun.jna.ptr.IntByReference;
+import com.sun.jna.win32.StdCallLibrary;
+
+/**
+ * JNA interface to Windows Kernel32 exports.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public interface Kernel32
+ extends StdCallLibrary
+{
+ //~ Instance fields --------------------------------------------------------
+
+ Kernel32 INSTANCE = Kernel32Utils.load();
+ int MOVEFILE_COPY_ALLOWED = 2;
+ int MOVEFILE_CREATE_HARDLINK = 16;
+ int MOVEFILE_DELAY_UNTIL_REBOOT = 4;
+ int MOVEFILE_FAIL_IF_NOT_TRACKABLE = 32;
+ int MOVEFILE_REPLACE_EXISTING = 1;
+ int MOVEFILE_WRITE_THROUGH = 8;
+ int FILE_ATTRIBUTE_REPARSE_POINT = 0x400;
+ int SYMBOLIC_LINK_FLAG_DIRECTORY = 1;
+ int STILL_ACTIVE = 259;
+
+ //~ Methods ----------------------------------------------------------------
+
+ /** HB */
+ WString GetCommandLineW();
+
+ /** HB */
+ int GetModuleFileNameA (Pointer hModule, byte[] lpFilename, int nSize);
+
+ /**
+ * Creates a symbolic link.
+ *
+ * Windows Vista+, Windows Server 2008+
+ *
+ * @param lpSymlinkFileName
+ * Symbolic link to be created
+ * @param lpTargetFileName
+ * Target of the link.
+ * @param dwFlags
+ * 0 or {@link #SYMBOLIC_LINK_FLAG_DIRECTORY}
+ * @see MSDN
+ */
+ boolean CreateSymbolicLinkW (WString lpSymlinkFileName,
+ WString lpTargetFileName,
+ int dwFlags);
+
+ boolean GetExitCodeProcess (Pointer handle,
+ IntByReference r);
+
+ int GetFileAttributesW (WString lpFileName);
+
+ /**
+ * See http://msdn.microsoft.com/en-us/library/aa365240(VS.85).aspx
+ */
+ boolean MoveFileExA (String existingFileName,
+ String newFileName,
+ int flags);
+
+ int WaitForSingleObject (Pointer handle,
+ int milliseconds);
+
+ // DWORD == int
+}
diff --git a/src/installer/hudson/util/jna/Kernel32Utils.java b/src/installer/hudson/util/jna/Kernel32Utils.java
new file mode 100644
index 0000000..cd509ee
--- /dev/null
+++ b/src/installer/hudson/util/jna/Kernel32Utils.java
@@ -0,0 +1,136 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2010, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.util.jna;
+
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.WString;
+import com.sun.jna.ptr.IntByReference;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class Kernel32Utils
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ private static final Logger LOGGER = Logger.getLogger(
+ Kernel32Utils.class.getName());
+
+ //~ Methods ----------------------------------------------------------------
+ /**
+ * @param target
+ * If relative, resolved against the location of the symlink.
+ * If absolute, it's absolute.
+ */
+ public static void createSymbolicLink (File symlink,
+ String target,
+ boolean dirLink)
+ throws IOException
+ {
+ if (!Kernel32.INSTANCE.CreateSymbolicLinkW(
+ new WString(symlink.getPath()),
+ new WString(target),
+ dirLink ? Kernel32.SYMBOLIC_LINK_FLAG_DIRECTORY : 0)) {
+ throw new WinIOException(
+ "Failed to create a symlink " + symlink + " to " + target);
+ }
+ }
+
+ public static int getWin32FileAttributes (File file)
+ throws IOException
+ {
+ // allow lookup of paths longer than MAX_PATH
+ // http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
+ String canonicalPath = file.getCanonicalPath();
+ String path;
+
+ if (canonicalPath.length() < 260) {
+ // path is short, use as-is
+ path = canonicalPath;
+ } else if (canonicalPath.startsWith("\\\\")) {
+ // network share
+ // \\server\share --> \\?\UNC\server\share
+ path = "\\\\?\\UNC\\" + canonicalPath.substring(2);
+ } else {
+ // prefix, canonical path should be normalized and absolute so this should work.
+ path = "\\\\?\\" + canonicalPath;
+ }
+
+ return Kernel32.INSTANCE.GetFileAttributesW(new WString(path));
+ }
+
+ public static boolean isJunctionOrSymlink (File file)
+ throws IOException
+ {
+ return (file.exists()
+ && ((Kernel32.FILE_ATTRIBUTE_REPARSE_POINT
+ & getWin32FileAttributes(file)) != 0));
+ }
+
+ /**
+ * Given the process handle, waits for its completion and returns the exit
+ * code.
+ */
+ public static int waitForExitProcess (Pointer hProcess)
+ throws InterruptedException
+ {
+ while (true) {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
+ Kernel32.INSTANCE.WaitForSingleObject(hProcess, 1000);
+
+ IntByReference exitCode = new IntByReference();
+ exitCode.setValue(-1);
+ Kernel32.INSTANCE.GetExitCodeProcess(hProcess, exitCode);
+
+ int v = exitCode.getValue();
+
+ if (v != Kernel32.STILL_ACTIVE) {
+ return v;
+ }
+ }
+ }
+
+ /* package */ static Kernel32 load ()
+ {
+ try {
+ return (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
+ } catch (Throwable e) {
+ LOGGER.log(Level.SEVERE, "Failed to load Kernel32", e);
+
+ return InitializationErrorInvocationHandler.create(
+ Kernel32.class,
+ e);
+ }
+ }
+}
diff --git a/src/installer/hudson/util/jna/SHELLEXECUTEINFO.java b/src/installer/hudson/util/jna/SHELLEXECUTEINFO.java
new file mode 100644
index 0000000..0f9062e
--- /dev/null
+++ b/src/installer/hudson/util/jna/SHELLEXECUTEINFO.java
@@ -0,0 +1,80 @@
+//----------------------------------------------------------------------------//
+// //
+// S H E L L E X E C U T E I N F O //
+// //
+//----------------------------------------------------------------------------//
+package hudson.util.jna;
+
+import com.sun.jna.Pointer;
+import com.sun.jna.Structure;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ *
+ *
+ typedef struct _SHELLEXECUTEINFO {
+ DWORD cbSize;
+ ULONG fMask;
+ HWND hwnd;
+ LPCTSTR lpVerb;
+ LPCTSTR lpFile;
+ LPCTSTR lpParameters;
+ LPCTSTR lpDirectory;
+ int nShow;
+ HINSTANCE hInstApp;
+ LPVOID lpIDList;
+ LPCTSTR lpClass;
+ HKEY hkeyClass;
+ DWORD dwHotKey;
+ union {
+ HANDLE hIcon;
+ HANDLE hMonitor;
+ } DUMMYUNIONNAME;
+ HANDLE hProcess;
+ } SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;
+ *
+ * @author Kohsuke Kawaguchi
+ * see http://msdn.microsoft.com/en-us/library/windows/desktop/bb759784%28v=vs.85%29.aspx
+ */
+public class SHELLEXECUTEINFO
+ extends Structure
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ public static final int SEE_MASK_NOCLOSEPROCESS = 0x40;
+ public static final int SW_HIDE = 0;
+ public static final int SW_SHOW = 0;
+
+ //~ Instance fields --------------------------------------------------------
+
+ public int cbSize = size();
+ public int fMask;
+ public Pointer hwnd;
+ public String lpVerb;
+ public String lpFile;
+ public String lpParameters;
+ public String lpDirectory;
+ public int nShow = 1;
+ public Pointer hInstApp;
+ public Pointer lpIDList;
+ public String lpClass;
+ public Pointer hkeyClass;
+ public int dwHotKey;
+ public Pointer hIcon;
+ public Pointer hProcess;
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override
+ protected List getFieldOrder ()
+ {
+ return Arrays.asList(
+ new String[] {
+ "cbSize", "fMask", "hwnd", "lpVerb", "lpFile", "lpParameters",
+ "lpDirectory", "nShow", "hInstApp", "lpIDList", "lpClass",
+ "hkeyClass", "dwHotKey", "hIcon", "hProcess"
+ });
+ }
+}
diff --git a/src/installer/hudson/util/jna/Shell32.java b/src/installer/hudson/util/jna/Shell32.java
new file mode 100644
index 0000000..c019a69
--- /dev/null
+++ b/src/installer/hudson/util/jna/Shell32.java
@@ -0,0 +1,44 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2010, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.util.jna;
+
+import com.sun.jna.Native;
+import com.sun.jna.win32.StdCallLibrary;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public interface Shell32 extends StdCallLibrary {
+ Shell32 INSTANCE = (Shell32) Native.loadLibrary("shell32", Shell32.class);
+
+ /**
+ * @return true if successful. Otherwise false.
+ */
+ boolean ShellExecuteEx(SHELLEXECUTEINFO lpExecInfo);
+
+ /**
+ * @return true if successful. Otherwise false.
+ */
+ boolean IsUserAnAdmin();
+}
diff --git a/src/installer/hudson/util/jna/WinIOException.java b/src/installer/hudson/util/jna/WinIOException.java
new file mode 100644
index 0000000..034853e
--- /dev/null
+++ b/src/installer/hudson/util/jna/WinIOException.java
@@ -0,0 +1,76 @@
+
+package hudson.util.jna;
+
+import com.sun.jna.Native;
+
+///import hudson.Util;
+import java.io.IOException;
+
+/**
+ * IOException originated from Windows API call.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class WinIOException
+ extends IOException
+{
+ //~ Instance fields --------------------------------------------------------
+
+ private final int errorCode = Native.getLastError();
+
+ //~ Constructors -----------------------------------------------------------
+
+ /**
+ * Creates a new WinIOException object.
+ */
+ public WinIOException ()
+ {
+ }
+
+ /**
+ * Creates a new WinIOException object.
+ *
+ * @param message DOCUMENT ME!
+ */
+ public WinIOException (String message)
+ {
+ super(message);
+ }
+
+ /**
+ * Creates a new WinIOException object.
+ *
+ * @param message DOCUMENT ME!
+ * @param cause DOCUMENT ME!
+ */
+ public WinIOException (String message,
+ Throwable cause)
+ {
+ super(message);
+ initCause(cause);
+ }
+
+ /**
+ * Creates a new WinIOException object.
+ *
+ * @param cause DOCUMENT ME!
+ */
+ public WinIOException (Throwable cause)
+ {
+ initCause(cause);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ public int getErrorCode ()
+ {
+ return errorCode;
+ }
+
+ @Override
+ public String getMessage ()
+ {
+ ///return super.getMessage()+" error="+errorCode+":"+ Util.getWin32ErrorMessage(errorCode);
+ return super.getMessage() + " error=" + errorCode;
+ }
+}
diff --git a/src/installer/hudson/util/jna/package.html b/src/installer/hudson/util/jna/package.html
new file mode 100644
index 0000000..75eb283
--- /dev/null
+++ b/src/installer/hudson/util/jna/package.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Package hudson.util.jna
+
+
+
+
+ JNA utilities meant for native access.
+
+
+
+
diff --git a/src/installer/overview.html b/src/installer/overview.html
new file mode 100644
index 0000000..1a96c97
--- /dev/null
+++ b/src/installer/overview.html
@@ -0,0 +1,127 @@
+
+
+
+ Audiveris Installer overview
+
+
+
+ Application and installer
+
+ Audiveris is now made available through the Java Web Start
+ technology.
+ Any launch of Audiveris is thus performed through a program called
+ javaws which is provided with launch.jnlp ,
+ a file which describes how Audiveris is to be launched.
+
+
+ The very first time Audiveris is launched, a specific companion of
+ Audiveris (named "installer") is run once before the application is
+ launched. The following times, only the application is launched.
+ The purpose of this installer is to check the user environment and
+ install missing components as needed (additional software and data).
+
+
+ The following diagram depicts how this is "orchestrated" by
+ javaws.
+ Only the relevant components are shown, especially to point out the
+ differences implied by the fact that the chosen javaws is a 32-bit
+ or a 64-bit version.
+ In the diagram, the left column refers to 32-bit Java and the right
+ column to 64-bit Java.
+
+
+ The specific version of javaws can be chosen:
+
+
+
+ From the command line when selecting javaws from
+ proper Java environment.
+
+ From Audiveris desktop shortcut.
+ This depends on which javaws file the Audiveris.lnk file points
+ to, and can be changed by modifying the shortcut properties.
+
+ Implicitly when clicking on the "Launch" button of Audiveris
+ web site: it's up to the web browser to go the 32 or 64 route.
+
+
+
+
+ The corresponding file locations are typically those shown by
+ Windows 7, 64-bit, where both Java environments have been installed:
+ "Program Files (x86)" vs "Program Files",
+ "Windows\SysWOW64" vs "Windows\System32", etc.
+ Roughly speaking, on a 32-bit Windows version (which can run only
+ a 32-bit Java environment), the 32-bit folders have the names of the
+ corresponding 64-bit folders on a 64-bit Windows.
+
+
+
+ Internal installer architecture
+
+ The Audiveris installer is organized around one core package
+ (com.audiveris.installer) and as many sub-packages as needed to
+ address different OSes.
+
+
+
+
+ Descriptors to support OS'es
+
+
+ The bulk of Installer processing is handled by classes from
+ com.audiveris.installer package, especially:
+
+
+ Installer
+ the main class which drives the installation or uninstallation,
+ Bundle
+ which handles the collection of companions to process,
+ Companion
+ which handles the installation/uninstallation of a given software
+ companion.
+
+
+
+ The specificities of a particular OS environment are meant to be
+ implemented in a proper sub-class of
+ Descriptor class.
+
+ For example, WindowsDescriptor
+ is the sub-class that implements the Windows descriptor.
+
+
+
+ The Descriptor class has been carefully designed, and should be the
+ only source to be extended by subclassing, to support OS'es like
+ Unix or Mac.
+
+
+ Jnlp to encapsulate JNLP services
+
+
+ The Jnlp class is
+ meant to hide the underlying Java Web start environment and also to
+ allow the running and debugging of the Installer even without any
+ Java Web Start environment available underneath.
+
+
+
+ It provides a selection of JNLP services:
+
+
+
+ BasicService for access to code base and web browser.
+ ExtensionInstallerService for reporting progress of the
+ Installer to the Java Web Start,
+ DownloadService which should be used to monitor downloading.
+ However, it requires all potentially downloadable URLs to be
+ explicitly referred to by the JNLP file and is also said to be
+ limited to the download of .jar files
+ (while Tesseract language files have a .tar.gz extension).
+ To be further investigated.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/Audiveris.java b/src/main/Audiveris.java
new file mode 100644
index 0000000..4c8890b
--- /dev/null
+++ b/src/main/Audiveris.java
@@ -0,0 +1,49 @@
+//----------------------------------------------------------------------------//
+// //
+// A u d i v e r i s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+import omr.WellKnowns;
+
+/**
+ * Class {@code Audiveris} is simply the entry point to OMR, which
+ * delegates the call to {@link omr.Main#doMain}.
+ *
+ * @author Hervé Bitteur
+ */
+public final class Audiveris
+{
+ //~ Constructors -----------------------------------------------------------
+
+ //-----------//
+ // Audiveris //
+ //-----------//
+ /** To avoid instantiation */
+ private Audiveris ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //------//
+ // main //
+ //------//
+ /**
+ * The main entry point, which just calls {@link omr.Main#doMain}.
+ *
+ * @param args These args are simply passed to Main
+ */
+ public static void main (final String[] args)
+ {
+ // We need class WellKnowns to be elaborated before class Main
+ WellKnowns.ensureLoaded();
+
+ // Then we call Main...
+ omr.Main.doMain(args);
+ }
+}
diff --git a/src/main/omr/CLI.java b/src/main/omr/CLI.java
new file mode 100644
index 0000000..1c58b1f
--- /dev/null
+++ b/src/main/omr/CLI.java
@@ -0,0 +1,676 @@
+//----------------------------------------------------------------------------//
+// //
+// C L I //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr;
+
+import omr.step.Step;
+import omr.step.Steps;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Class {@code CLI} parses and holds the parameters of the command
+ * line interface.
+ *
+ * The command line parameters can be (order and case are not relevant):
+ *
+ *
+ * -help to print a quick usage help and leave the
+ * application.
+ *
+ * -batch to run in batch mode, with no user
+ * interface.
+ *
+ * -step (STEPNAME | @STEPLIST)+ to run all the
+ * specified steps (including the steps which are mandatory to get to the
+ * specified ones). 'STEPNAME' can be any one of the step names (the case is
+ * irrelevant) as defined in the {@link Steps} class. These steps will
+ * be performed on each sheet referenced from the command line.
+ *
+ * -option (KEY=VALUE | @OPTIONLIST)+ to specify
+ * the value of some application parameters (that can also be set via the
+ * pull-down menu "Tools|Options"), either by stating the key=value pair or by
+ * referencing (flagged by a @ sign) a file that lists key=value pairs (or
+ * even other files list recursively).
+ * A list file is a simple text file, with one key=value pair per line.
+ * Nota : The syntax used is the Properties syntax, so for example
+ * back-slashes must be escaped.
+ *
+ * -script (SCRIPTNAME | @SCRIPTLIST)+ to specify
+ * some scripts to be read, using the same mechanism than input command belows.
+ * These script files contain actions generally recorded during a previous run.
+ *
+ *
+ * -input (FILENAME | @FILELIST)+ to specify some
+ * image files to be read, either by naming the image file or by referencing
+ * (flagged by a @ sign) a file that lists image files (or even other files
+ * list recursively).
+ * A list file is a simple text file, with one image file name per line.
+ *
+ * -pages (PAGE | @PAGELIST)+ to specify some
+ * specific pages, counted from 1, to be loaded out of the input file.
+ *
+ * -bench (DIRNAME | FILENAME) to define an output
+ * path to bench data file (or directory).
+ * Nota : If the path refers to an existing directory, each processed
+ * score will output its bench data to a score-specific file created in the
+ * provided directory. Otherwise, all bench data, whatever its related score,
+ * will be written to the provided single file.
+ *
+ *
+ * -midi (DIRNAME | FILENAME) to define an output
+ * path to MIDI file (or directory). Same note as for -bench.
+ *
+ *
+ * -print (DIRNAME | FILENAME) to define an output
+ * path to PDF file (or directory). Same note as for -bench.
+ *
+ * -export (DIRNAME | FILENAME) to define an output
+ * path to MusicXML file (or directory). Same note as for -bench.
+ *
+ *
+ *
+ * @author Hervé Bitteur
+ */
+public class CLI
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(CLI.class);
+
+ //~ Enumerations -----------------------------------------------------------
+ /** For handling cardinality of command parameters */
+ private static enum Card
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ /** No parameter expected */
+ NONE,
+ /** Just a single parameter is
+ * expected */
+ SINGLE,
+ /** One or several
+ * parameters are expected */
+ MULTIPLE;
+
+ }
+
+ /** For command analysis */
+ private static enum Command
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ HELP(
+ "Prints help about application arguments and stops",
+ Card.NONE,
+ null),
+ BATCH(
+ "Specifies to run with no graphic user interface",
+ Card.NONE,
+ null),
+ STEP(
+ "Defines a series of target steps",
+ Card.MULTIPLE,
+ "(STEPNAME|@STEPLIST)+"),
+ OPTION(
+ "Defines a series of key=value constant pairs",
+ Card.MULTIPLE,
+ "(KEY=VALUE|@OPTIONLIST)+"),
+ SCRIPT(
+ "Defines a series of script files to run",
+ Card.MULTIPLE,
+ "(SCRIPTNAME|@SCRIPTLIST)+"),
+ INPUT(
+ "Defines a series of input image files to process",
+ Card.MULTIPLE,
+ "(FILENAME|@FILELIST)+"),
+ PAGES(
+ "Defines a set of specific pages to process",
+ Card.MULTIPLE,
+ "(PAGE|@PAGELIST)+"),
+ BENCH(
+ "Defines an output path to bench data file (or directory)",
+ Card.SINGLE,
+ "(DIRNAME|FILENAME)"),
+ // MIDI(
+ // "Defines an output path to MIDI file (or directory)",
+ // Card.SINGLE,
+ // "(DIRNAME|FILENAME)"),
+ PRINT(
+ "Defines an output path to PDF file (or directory)",
+ Card.SINGLE,
+ "(DIRNAME|FILENAME)"),
+ EXPORT(
+ "Defines an output path to MusicXML file (or directory)",
+ Card.SINGLE,
+ "(DIRNAME|FILENAME)");
+ //~ Instance fields ----------------------------------------------------
+
+ /** Info about command itself */
+ public final String description;
+
+ /** Cardinality of the expected parameters */
+ public final Card card;
+
+ /** Info about expected command parameters */
+ public final String params;
+
+ //~ Constructors -------------------------------------------------------
+ /**
+ * Creates a Command object.
+ *
+ * @param description description of the command
+ * @param params description of command expected parameters, or
+ * null
+ */
+ Command (String description,
+ Card card,
+ String params)
+ {
+ this.description = description;
+ this.card = card;
+ this.params = params;
+ }
+ }
+
+ //~ Instance fields --------------------------------------------------------
+ /** Name of the program */
+ private final String toolName;
+
+ /** The CLI arguments */
+ private final String[] args;
+
+ /** The parameters to fill */
+ private final Parameters parameters;
+
+ //~ Constructors -----------------------------------------------------------
+ //-----//
+ // CLI //
+ //-----//
+ /**
+ * Creates a new CLI object.
+ *
+ * @param toolName the program name
+ * @param args the CLI arguments
+ */
+ public CLI (final String toolName,
+ final String... args)
+ {
+ this.toolName = toolName;
+ this.args = Arrays.copyOf(args, args.length);
+ logger.debug("CLI args: {}", Arrays.toString(args));
+
+ parameters = parse();
+
+ if (parameters != null) {
+ parameters.setImpliedSteps();
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //---------------//
+ // getParameters //
+ //---------------//
+ /**
+ * Parse the CLI arguments and return the populated parameters
+ * structure.
+ *
+ * @return the parsed parameters, or null if failed
+ */
+ public Parameters getParameters ()
+ {
+ return parameters;
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ for (String arg : args) {
+ sb.append(" ").append(arg);
+ }
+
+ return sb.toString();
+ }
+
+ //---------//
+ // addItem //
+ //---------//
+ /**
+ * Add an item to a provided list, while handling indirections if
+ * needed.
+ *
+ * @param item the item to add, which can be a plain string (which is
+ * simply added to the list) or an indirection
+ * (a string starting by the '@' character)
+ * which denotes a file of items to be recursively added
+ * @param list the collection of items to be augmented
+ */
+ private void addItem (String item,
+ List list)
+ {
+ // The item may be a plain string or the name of a pack that lists
+ // item(s). This is signalled by a starting '@' character in string
+ if (item.startsWith("@")) {
+ // File with other items inside
+ String pack = item.substring(1);
+ BufferedReader br = null;
+
+ try {
+ br = new BufferedReader(new FileReader(pack));
+
+ String newRef;
+
+ try {
+ while ((newRef = br.readLine()) != null) {
+ addItem(newRef.trim(), list);
+ }
+
+ br.close();
+ } catch (IOException ex) {
+ logger.warn("IO error while reading file ''{}''", pack);
+ }
+ } catch (FileNotFoundException ex) {
+ logger.warn("Cannot find file ''{}''", pack);
+ } finally {
+ if (br != null) {
+ try {
+ br.close();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+ } else if (item.length() > 0) {
+ // Plain item
+ list.add(item);
+ }
+ }
+
+ //-----------------//
+ // decodeConstants //
+ //-----------------//
+ /**
+ * Retrieve properties out of the flat sequence of "key = value"
+ * pairs.
+ *
+ * @param constantPairs the flat sequence of key = value pairs
+ * @return the resulting constant properties
+ */
+ private Properties decodeConstants (List constantPairs)
+ throws IOException
+ {
+ Properties props = new Properties();
+
+ // Use a simple string buffer in memory
+ StringBuilder sb = new StringBuilder();
+
+ for (String pair : constantPairs) {
+ sb.append(pair).append("\n");
+ }
+
+ props.load(new StringReader(sb.toString()));
+
+ return props;
+ }
+
+ //-------//
+ // parse //
+ //-------//
+ /**
+ * Parse the CLI arguments and populate the parameters structure.
+ *
+ * @return the populated parameters structure, or null if failed
+ */
+ private Parameters parse ()
+ {
+ // Status of the finite state machine
+ boolean paramNeeded = false; // Are we expecting a param?
+ boolean paramForbidden = false; // Are we not expecting a param?
+ Command command = Command.INPUT; // By default
+ Parameters params = new Parameters();
+ List optionPairs = new ArrayList<>();
+ List stepStrings = new ArrayList<>();
+ List pageStrings = new ArrayList<>();
+
+ // Parse all arguments from command line
+ for (int i = 0; i < args.length; i++) {
+ String token = args[i];
+
+ if (token.startsWith("-")) {
+ // This is a new command
+ // Check that we were not expecting param(s)
+ if (paramNeeded) {
+ printCommandLine();
+ stopUsage(
+ "Found no parameter after command '" + command + "'");
+
+ return null;
+ }
+
+ // Remove leading minus sign and switch to uppercase
+ // To recognize command
+ token = token.substring(1).toUpperCase(Locale.ENGLISH);
+
+ boolean found = false;
+
+ for (Command cmd : Command.values()) {
+ if (token.equals(cmd.name())) {
+ command = cmd;
+ paramNeeded = command.card != Card.NONE;
+ paramForbidden = !paramNeeded;
+ found = true;
+
+ break;
+ }
+ }
+
+ // No command recognized
+ if (!found) {
+ printCommandLine();
+ stopUsage("Unknown command '-" + token + "'");
+
+ return null;
+ }
+
+ // Commands with no parameters
+ switch (command) {
+ case HELP: {
+ stopUsage(null);
+
+ return null;
+ }
+
+ case BATCH:
+ params.batchMode = true;
+
+ break;
+ }
+ } else {
+ // This is a parameter for the current command
+ // Check we can accept a parameter
+ if (paramForbidden) {
+ printCommandLine();
+ stopUsage(
+ "Extra parameter '" + token
+ + "' found after command '" + command + "'");
+
+ return null;
+ }
+
+ switch (command) {
+ case STEP:
+ addItem(token, stepStrings);
+
+ break;
+
+ case OPTION:
+ addItem(token, optionPairs);
+
+ break;
+
+ case SCRIPT:
+ addItem(token, params.scriptNames);
+
+ break;
+
+ case INPUT:
+ addItem(token, params.inputNames);
+
+ break;
+
+ case PAGES:
+ addItem(token, pageStrings);
+
+ break;
+
+ case BENCH:
+ params.benchPath = token;
+
+ break;
+
+ case EXPORT:
+ params.exportPath = token;
+
+ break;
+
+ // case MIDI :
+ // params.midiPath = token;
+ //
+ // break;
+ case PRINT:
+ params.printPath = token;
+
+ break;
+
+ default:
+ }
+
+ paramNeeded = false;
+ paramForbidden = command.card == Card.SINGLE;
+ }
+ }
+
+ // Additional error checking
+ if (paramNeeded) {
+ printCommandLine();
+ stopUsage("Expecting a token after command '" + command + "'");
+
+ return null;
+ }
+
+ // Decode option pairs
+ try {
+ params.options = decodeConstants(optionPairs);
+ } catch (Exception ex) {
+ logger.warn("Error decoding -option ", ex);
+ }
+
+ // Check step names
+ for (String stepString : stepStrings) {
+ try {
+ // Read a step name
+ params.desiredSteps.add(
+ Steps.valueOf(stepString.toUpperCase()));
+ } catch (Exception ex) {
+ printCommandLine();
+ stopUsage(
+ "Step name expected, found '" + stepString + "' instead");
+
+ return null;
+ }
+ }
+
+ // Check page ids
+ for (String pageString : pageStrings) {
+ try {
+ // Read a page id (counted from 1)
+ int id = Integer.parseInt(pageString);
+ if (params.pages == null) {
+ params.pages = new TreeSet<>();
+ }
+ params.pages.add(id);
+ } catch (Exception ex) {
+ printCommandLine();
+ stopUsage(
+ "Page id expected, found '" + pageString + "' instead");
+
+ return null;
+ }
+ }
+
+ // Results
+ logger.debug(Main.dumping.dumpOf(params));
+
+ return params;
+ }
+
+ //------------------//
+ // printCommandLine //
+ //------------------//
+ /**
+ * Printout the command line with its actual parameters.
+ */
+ private void printCommandLine ()
+ {
+ StringBuilder sb = new StringBuilder("Command line parameters: ");
+
+ if (toolName != null) {
+ sb.append(toolName).append(" ");
+ }
+
+ sb.append(this);
+ logger.info(sb.toString());
+ }
+
+ //-----------//
+ // stopUsage //
+ //-----------//
+ /**
+ * Printout a message if any, followed by the general syntax for
+ * the command line.
+ *
+ * @param msg the message to print if non null
+ */
+ private void stopUsage (String msg)
+ {
+ // Print message if any
+ if (msg != null) {
+ logger.warn(msg);
+ }
+
+ StringBuilder buf = new StringBuilder();
+
+ // Print version
+ buf.append("\nVersion:");
+ buf.append("\n ").append(WellKnowns.TOOL_REF);
+
+ // Print arguments syntax
+ buf.append("\nArguments syntax:");
+
+ for (Command command : Command.values()) {
+ buf.append(
+ String.format(
+ "%n %-36s %s",
+ String.format(
+ " [-%s%s]",
+ command.toString().toLowerCase(Locale.ENGLISH),
+ ((command.params != null) ? (" " + command.params) : "")),
+ command.description));
+ }
+
+ // Print all allowed step names
+ buf.append("\n\nKnown step names are in order").append(
+ " (non case-sensitive):");
+
+ for (Step step : Steps.values()) {
+ buf.append(
+ String.format(
+ "%n %-11s : %s",
+ step.toString().toUpperCase(),
+ step.getDescription()));
+ }
+
+ logger.info(buf.toString());
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //------------//
+ // Parameters //
+ //------------//
+ /**
+ * A structure that collects the various parameters parsed out of
+ * the command line.
+ */
+ public static class Parameters
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** Flag that indicates a batch mode */
+ boolean batchMode = false;
+
+ /** The set of desired steps */
+ final Set desiredSteps = new LinkedHashSet<>();
+
+ /** The map of options */
+ Properties options = null;
+
+ /** The list of script file names to execute */
+ final List scriptNames = new ArrayList<>();
+
+ /** The list of input image file names to load */
+ final List inputNames = new ArrayList<>();
+
+ /** The set of page ids to load */
+ SortedSet pages = null;
+
+ /** Where log data is to be saved */
+ ///String logPath = null;
+ /** Where bench data is to be saved */
+ String benchPath = null;
+
+ /** Where exported score data (MusicXML) is to be saved */
+ String exportPath = null;
+
+ /** Where MIDI data is to be saved */
+ String midiPath = null;
+
+ /** Where printed score (PDF) is to be saved */
+ String printPath = null;
+
+ //~ Constructors -------------------------------------------------------
+ private Parameters ()
+ {
+ }
+
+ //~ Methods ------------------------------------------------------------
+ //-----------------//
+ // setImpliedSteps //
+ //-----------------//
+ /**
+ * Some output parameters require their related step to be set.
+ */
+ private void setImpliedSteps ()
+ {
+ if (exportPath != null) {
+ desiredSteps.add(Steps.valueOf(Steps.EXPORT));
+ }
+
+ if (printPath != null) {
+ desiredSteps.add(Steps.valueOf(Steps.PRINT));
+ }
+
+ // if (midiPath != null) {
+ // desiredSteps.add(Steps.valueOf(Steps.MIDI));
+ // }
+ }
+ }
+}
diff --git a/src/main/omr/Debug.java b/src/main/omr/Debug.java
new file mode 100644
index 0000000..44aa788
--- /dev/null
+++ b/src/main/omr/Debug.java
@@ -0,0 +1,173 @@
+//----------------------------------------------------------------------------//
+// //
+// D e b u g //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr;
+
+import omr.glyph.GlyphRepository;
+import omr.glyph.Shape;
+import omr.glyph.ShapeDescription;
+import omr.glyph.ShapeSet;
+import omr.glyph.facets.Glyph;
+
+import omr.score.ui.ScoreDependent;
+
+import org.jdesktop.application.Action;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.event.ActionEvent;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Convenient class meant to temporarily inject some debugging.
+ * To be used in sync with file user-actions.xml in config folder
+ *
+ * @author Hervé Bitteur
+ */
+public class Debug
+ extends ScoreDependent
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Debug.class);
+
+ //~ Methods ----------------------------------------------------------------
+ // //------------------//
+ // // injectChordNames //
+ // //------------------//
+ // @Action(enabledProperty = SHEET_AVAILABLE)
+ // public void injectChordNames (ActionEvent e)
+ // {
+ // Score score = ScoreController.getCurrentScore();
+ //
+ // if (score == null) {
+ // return;
+ // }
+ //
+ // ScoreSystem system = score.getFirstPage()
+ // .getFirstSystem();
+ // system.acceptChildren(new ChordInjector());
+ // }
+ // //---------------//
+ // // ChordInjector //
+ // //---------------//
+ // private static class ChordInjector
+ // extends AbstractScoreVisitor
+ // {
+ // //~ Static fields/initializers -----------------------------------------
+ //
+ // /** List of symbols to inject. */
+ // private static final String[] shelf = new String[] {
+ // "BMaj7/D#", "BMaj7", "G#m9",
+ // "F#", "C#7sus4", "F#"
+ // };
+ //
+ // //~ Instance fields ----------------------------------------------------
+ //
+ // /** Current index to symbol to inject. */
+ // private int symbolCount = 0;
+ //
+ // //~ Methods ------------------------------------------------------------
+ //
+ // @Override
+ // public boolean visit (ChordSymbol symbol)
+ // {
+ // // Replace chord info by one taken from the shelf
+ // if (symbolCount < shelf.length) {
+ // symbol.info = ChordInfo.create(shelf[symbolCount++]);
+ // }
+ //
+ // return false;
+ // }
+ // }
+ //------------------//
+ // saveTrainingData //
+ //------------------//
+ @Action
+ public void saveTrainingData (ActionEvent e)
+ {
+ final PrintWriter out = getPrintWriter(new File("glyphs.arff"));
+
+ out.println("@relation " + "glyphs");
+ out.println();
+
+ for (String label : ShapeDescription.getParameterLabels()) {
+ out.println("@attribute " + label + " real");
+ }
+
+ // Last attribute: shape
+ out.print("@attribute shape {");
+
+ for (Shape shape : ShapeSet.allPhysicalShapes) {
+ out.print(shape);
+
+ if (shape != Shape.LAST_PHYSICAL_SHAPE) {
+ out.print(", ");
+ }
+ }
+
+ out.println("}");
+
+ out.println();
+ out.println("@data");
+
+ GlyphRepository repository = GlyphRepository.getInstance();
+ List gNames = repository.getWholeBase(null);
+ logger.info("Glyphs: {}", gNames.size());
+
+ for (String gName : gNames) {
+ Glyph glyph = repository.getGlyph(gName, null);
+
+ if (glyph != null) {
+ double[] ins = ShapeDescription.features(glyph);
+
+ for (double in : ins) {
+ out.print((float) in);
+ out.print(",");
+ }
+
+ out.println(glyph.getShape().getPhysicalShape());
+
+ //break; /////////////////////////////////////////////////////////
+ }
+ }
+
+ out.flush();
+ out.close();
+ logger.info("Done.");
+ }
+
+ //----------------//
+ // getPrintWriter //
+ //----------------//
+ private static PrintWriter getPrintWriter (File file)
+ {
+ try {
+ final BufferedWriter bw = new BufferedWriter(
+ new OutputStreamWriter(
+ new FileOutputStream(file),
+ WellKnowns.FILE_ENCODING));
+
+ return new PrintWriter(bw);
+ } catch (Exception ex) {
+ System.err.println("Error creating " + file + ex);
+
+ return null;
+ }
+ }
+}
diff --git a/src/main/omr/Main.java b/src/main/omr/Main.java
new file mode 100644
index 0000000..b1142ee
--- /dev/null
+++ b/src/main/omr/Main.java
@@ -0,0 +1,548 @@
+//----------------------------------------------------------------------------//
+// //
+// M a i n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr;
+
+import omr.constant.Constant;
+import omr.constant.ConstantManager;
+import omr.constant.ConstantSet;
+
+import omr.score.Score;
+
+import omr.script.ScriptManager;
+
+import omr.step.ProcessingCancellationException;
+import omr.step.Stepping;
+
+import omr.ui.MainGui;
+import omr.ui.symbol.MusicFont;
+
+import omr.util.ClassUtil;
+import omr.util.Clock;
+import omr.util.Dumping;
+import omr.util.OmrExecutors;
+
+import org.jdesktop.application.Application;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.SortedSet;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class {@code Main} is the main class for OMR application.
+ * It deals with the main routine and its command line parameters.
+ * It launches the User Interface, unless a batch mode is selected.
+ *
+ * @see CLI
+ *
+ * @author Hervé Bitteur
+ */
+public class Main
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ static {
+ /** Time stamp */
+ Clock.resetTime();
+ }
+
+ /** Master View */
+ private static MainGui gui;
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Main.class);
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Parameters read from CLI */
+ private static CLI.Parameters parameters;
+
+ /** The application dumping service */
+ public static final Dumping dumping = new Dumping(Main.class.getPackage());
+
+ //~ Constructors -----------------------------------------------------------
+ //------//
+ // Main //
+ //------//
+ private Main ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //--------//
+ // doMain //
+ //--------//
+ /**
+ * Specific starting method for the application.
+ *
+ * @param args command line parameters
+ * @see omr.CLI the possible command line parameters
+ */
+ public static void doMain (String[] args)
+ {
+ // Initialize tool parameters
+ initialize();
+
+ // Process CLI arguments
+ process(args);
+
+ // Locale to be used in the whole application?
+ checkLocale();
+
+ // Environment
+ showEnvironment();
+
+ // Native libs
+ loadNativeLibraries();
+
+ if (!parameters.batchMode) {
+ // For interactive mode
+ logger.debug("Main. Launching MainGui");
+ Application.launch(MainGui.class, args);
+ } else {
+ // For batch mode
+
+ // Remember if at least one task failed
+ boolean failure = false;
+
+ // Check MusicFont is loaded
+ MusicFont.checkMusicFont();
+
+ // Launch the required tasks, if any
+ List> tasks = new ArrayList>();
+ tasks.addAll(getFilesTasks());
+ tasks.addAll(getScriptsTasks());
+
+ if (!tasks.isEmpty()) {
+ try {
+ logger.info("Submitting {} task(s)", tasks.size());
+
+ List> futures = OmrExecutors.getCachedLowExecutor()
+ .invokeAll(
+ tasks,
+ constants.processTimeOut.getValue(),
+ TimeUnit.SECONDS);
+ logger.info("Checking {} task(s)", tasks.size());
+
+ // Check for time-out
+ for (Future future : futures) {
+ try {
+ future.get();
+ } catch (Exception ex) {
+ logger.warn("Future exception", ex);
+ failure = true;
+ }
+ }
+ } catch (Exception ex) {
+ logger.warn("Error in processing tasks", ex);
+ failure = true;
+ }
+ }
+
+ // At this point all tasks have completed (normally or not)
+ // So shutdown immediately the executors
+ OmrExecutors.shutdown(true);
+
+ // Store latest constant values on disk?
+ if (constants.persistBatchCliConstants.getValue()) {
+ ConstantManager.getInstance()
+ .storeResource();
+ }
+
+ // Stop the JVM with failure status?
+ if (failure) {
+ logger.warn("Exit with failure status");
+ System.exit(-1);
+ }
+ }
+ }
+
+ //--------------//
+ // getBenchPath //
+ //--------------//
+ /**
+ * Report the bench path if present on the CLI
+ *
+ * @return the CLI bench path, or null
+ */
+ public static String getBenchPath ()
+ {
+ return parameters.benchPath;
+ }
+
+ //-----------------//
+ // getCliConstants //
+ //-----------------//
+ /**
+ * Report the properties set at the CLI level
+ *
+ * @return the CLI-defined constant values
+ */
+ public static Properties getCliConstants ()
+ {
+ if (parameters == null) {
+ return null;
+ } else {
+ return parameters.options;
+ }
+ }
+
+ //---------------//
+ // getExportPath //
+ //---------------//
+ /**
+ * Report the export path if present on the CLI
+ *
+ * @return the CLI export path, or null
+ */
+ public static String getExportPath ()
+ {
+ return parameters.exportPath;
+ }
+
+ //---------------//
+ // getFilesTasks //
+ //---------------//
+ /**
+ * Prepare the processing of image files listed on command line
+ *
+ * @return the collection of proper callables
+ */
+ public static List> getFilesTasks ()
+ {
+ List> tasks = new ArrayList>();
+
+ // Launch desired step on each score in parallel
+ for (final String name : parameters.inputNames) {
+ final File file = new File(name);
+
+ tasks.add(
+ new Callable()
+ {
+ @Override
+ public Void call ()
+ throws Exception
+ {
+ if (!parameters.desiredSteps.isEmpty()) {
+ logger.info(
+ "Launching {} on {} {}",
+ parameters.desiredSteps,
+ name,
+ (parameters.pages != null)
+ ? ("pages "
+ + parameters.pages)
+ : "");
+ }
+
+ if (file.exists()) {
+ final Score score = new Score(file);
+
+ try {
+ Stepping.processScore(
+ parameters.desiredSteps,
+ parameters.pages,
+ score);
+ } catch (ProcessingCancellationException pce) {
+ logger.warn("Cancelled " + score, pce);
+ score.getBench()
+ .recordCancellation();
+ throw pce;
+ } catch (Throwable ex) {
+ logger.warn("Exception occurred", ex);
+ throw ex;
+ } finally {
+ // Close (when in batch mode only)
+ if (gui == null) {
+ score.close();
+ }
+
+ return null;
+ }
+ } else {
+ String msg = "Could not find file "
+ + file.getCanonicalPath();
+ logger.warn(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+ });
+ }
+
+ return tasks;
+ }
+
+ //--------//
+ // getGui //
+ //--------//
+ /**
+ * Points to the single instance of the User Interface, if any.
+ *
+ * @return MainGui instance, which may be null
+ */
+ public static MainGui getGui ()
+ {
+ return gui;
+ }
+
+ //-------------//
+ // getMidiPath //
+ //-------------//
+ /**
+ * Report the midi path if present on the CLI
+ *
+ * @return the CLI midi path, or null
+ */
+ public static String getMidiPath ()
+ {
+ return parameters.midiPath;
+ }
+
+ //-------------//
+ // getPagesIds //
+ //-------------//
+ /**
+ * Report the set of page ids if present on the CLI
+ *
+ * @return the CLI page ids, or null
+ */
+ public static SortedSet getPageIds ()
+ {
+ return parameters.pages;
+ }
+
+ //--------------//
+ // getPrintPath //
+ //--------------//
+ /**
+ * Report the print path if present on the CLI
+ *
+ * @return the CLI print path, or null
+ */
+ public static String getPrintPath ()
+ {
+ return parameters.printPath;
+ }
+
+ //-----------------//
+ // getScriptsTasks //
+ //-----------------//
+ /**
+ * Prepare the processing of scripts listed on command line
+ *
+ * @return the collection of proper script callables
+ */
+ public static List> getScriptsTasks ()
+ {
+ List> tasks = new ArrayList>();
+
+ // Launch desired scripts in parallel
+ for (String name : parameters.scriptNames) {
+ final String scriptName = name;
+
+ tasks.add(
+ new Callable()
+ {
+ @Override
+ public Void call ()
+ throws Exception
+ {
+ ScriptManager.getInstance()
+ .loadAndRun(new File(scriptName));
+
+ return null;
+ }
+ });
+ }
+
+ return tasks;
+ }
+
+ //--------//
+ // setGui //
+ //--------//
+ /**
+ * Register the GUI (done by the GUI itself when it is ready)
+ *
+ * @param gui the MainGui instance
+ */
+ public static void setGui (MainGui gui)
+ {
+ Main.gui = gui;
+ }
+
+ //-------------//
+ // checkLocale //
+ //-------------//
+ private static void checkLocale ()
+ {
+ final String localeStr = constants.locale.getValue()
+ .trim();
+
+ if (!localeStr.isEmpty()) {
+ for (Locale locale : Locale.getAvailableLocales()) {
+ if (locale.toString()
+ .equalsIgnoreCase(localeStr)) {
+ Locale.setDefault(locale);
+ logger.debug("Locale set to {}", locale);
+
+ return;
+ }
+ }
+
+ logger.warn("Cannot set locale to {}", localeStr);
+ }
+ }
+
+ //------------//
+ // initialize //
+ //------------//
+ private static void initialize ()
+ {
+ // (re) Open the executor services
+ OmrExecutors.restart();
+ }
+
+ //---------------------//
+ // loadNativeLibraries //
+ //---------------------//
+ /**
+ * Explicitly load all the needed native libraries.
+ */
+ private static void loadNativeLibraries ()
+ {
+ // Explicitly load all native libs resources and in proper order
+ logger.info("Loading native libraries ...");
+
+ boolean success = true;
+
+ if (WellKnowns.WINDOWS) {
+ // For Windows, drop only the ".dll" suffix
+ success &= ClassUtil.loadLibrary("jniTessBridge");
+ success &= ClassUtil.loadLibrary("libtesseract302");
+ success &= ClassUtil.loadLibrary("liblept168");
+ } else if (WellKnowns.LINUX) {
+ // For Linux, drop both the "lib" prefix and the ".so" suffix
+ success &= ClassUtil.loadLibrary("jniTessBridge");
+ }
+
+ if (success) {
+ logger.info("All libraries loaded.");
+ } else {
+ // Inform user of OCR installation problem
+ String msg = "Tesseract OCR is not installed properly";
+
+ if (Main.getGui() != null) {
+ Main.getGui()
+ .displayError(msg);
+ } else {
+ logger.warn(msg);
+ }
+ }
+ }
+
+ //---------//
+ // process //
+ //---------//
+ private static void process (String[] args)
+ {
+ // First get the provided arguments if any
+ parameters = new CLI(WellKnowns.TOOL_NAME, args).getParameters();
+
+ if (parameters == null) {
+ logger.warn("Exiting ...");
+
+ // Stop the JVM, with failure status (1)
+ Runtime.getRuntime()
+ .exit(1);
+ }
+
+ // Interactive or Batch mode ?
+ if (parameters.batchMode) {
+ logger.info("Running in batch mode");
+
+ ///System.setProperty("java.awt.headless", "true");
+
+ // // Check MIDI output is not asked for
+ // Step midiStep = Steps.valueOf(Steps.MIDI);
+ //
+ // if ((midiStep != null) &&
+ // parameters.desiredSteps.contains(midiStep)) {
+ // logger.warn(
+ // "MIDI output is not compatible with -batch mode." +
+ // " MIDI output is ignored.");
+ // parameters.desiredSteps.remove(midiStep);
+ // }
+ } else {
+ logger.debug("Running in interactive mode");
+ }
+ }
+
+ //-----------------//
+ // showEnvironment //
+ //-----------------//
+ /**
+ * Show the application environment to the user.
+ */
+ private static void showEnvironment ()
+ {
+ if (constants.showEnvironment.isSet()) {
+ logger.info(
+ "Environment:\n" + "- Audiveris: {}\n"
+ + "- OS: {}\n" + "- Architecture: {}\n"
+ + "- Java VM: {}",
+ WellKnowns.TOOL_REF + ":" + WellKnowns.TOOL_BUILD,
+ System.getProperty("os.name") + " "
+ + System.getProperty("os.version"),
+ System.getProperty("os.arch"),
+ System.getProperty("java.vm.name") + " (build "
+ + System.getProperty("java.vm.version") + ", "
+ + System.getProperty("java.vm.info") + ")");
+ }
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-----------//
+ // Constants //
+ //-----------//
+ private static final class Constants
+ extends ConstantSet
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private final Constant.Boolean showEnvironment = new Constant.Boolean(
+ true,
+ "Should we show environment?");
+
+ private final Constant.String locale = new Constant.String(
+ "en",
+ "Locale language to be used in the whole application (en, fr)");
+
+ private final Constant.Boolean persistBatchCliConstants = new Constant.Boolean(
+ false,
+ "Should we persist CLI-defined constants when running in batch?");
+
+ private final Constant.Integer processTimeOut = new Constant.Integer(
+ "Seconds",
+ 300,
+ "Process time-out, specified in seconds");
+
+ }
+}
diff --git a/src/main/omr/WellKnowns.java b/src/main/omr/WellKnowns.java
new file mode 100644
index 0000000..0acdb1d
--- /dev/null
+++ b/src/main/omr/WellKnowns.java
@@ -0,0 +1,501 @@
+//----------------------------------------------------------------------------//
+// //
+// W e l l K n o w n s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr;
+
+import omr.log.LogUtil;
+import static omr.util.UriUtil.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.net.JarURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.jar.JarFile;
+
+/**
+ * Class {@code WellKnowns} gathers top public static final data to be
+ * shared within Audiveris application.
+ *
+ * Note that a few initial operations are performed here, because they need
+ * to take place before any other class is loaded.
+ *
+ * @author Hervé Bitteur
+ */
+public class WellKnowns
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ //----------//
+ // IDENTITY //
+ //----------//
+ //
+ /** Application company name: {@value}. */
+ public static final String COMPANY_NAME = ProgramId.COMPANY_NAME;
+
+ /** Application company id: {@value}. */
+ public static final String COMPANY_ID = ProgramId.COMPANY_ID;
+
+ /** Application name: {@value}. */
+ public static final String TOOL_NAME = ProgramId.NAME;
+
+ /** Application reference: {@value}. */
+ public static final String TOOL_REF = ProgramId.VERSION + "."
+ + ProgramId.REVISION;
+
+ /** Application build: {@value}. */
+ public static final String TOOL_BUILD = ProgramId.BUILD;
+
+ /** Specific prefix for application folders: {@value} */
+ private static final String TOOL_PREFIX = "/" + COMPANY_ID + "/"
+ + TOOL_NAME;
+
+ //----------//
+ // PLATFORM //
+ //----------//
+ //
+ /** Name of operating system */
+ private static final String OS_NAME = System.getProperty("os.name")
+ .toLowerCase(Locale.ENGLISH);
+
+ /** Are we using a Linux OS?. */
+ public static final boolean LINUX = OS_NAME.startsWith("linux");
+
+ /** Are we using a Mac OS?. */
+ public static final boolean MAC_OS_X = OS_NAME.startsWith("mac os x");
+
+ /** Are we using a Windows OS?. */
+ public static final boolean WINDOWS = OS_NAME.startsWith("windows");
+
+ /** Precise OS architecture. */
+ public static final String OS_ARCH = System.getProperty("os.arch");
+
+ /** Are we using Windows on 64 bit architecture?. */
+ public static final boolean WINDOWS_64 = WINDOWS
+ && (System.getenv(
+ "ProgramFiles(x86)") != null);
+
+ /** File character encoding. */
+ public static final String FILE_ENCODING = getFileEncoding();
+
+ /** File separator for the current platform. */
+ public static final String FILE_SEPARATOR = System.getProperty(
+ "file.separator");
+
+ /** Line separator for the current platform. */
+ public static final String LINE_SEPARATOR = System.getProperty(
+ "line.separator");
+
+ /** Redirection, if any, of standard out and err streams. */
+ public static final String STD_OUT_ERR = System.getProperty("stdouterr");
+
+ //---------//
+ // PROGRAM // This is a read-only area
+ //---------//
+ //
+ /** The container from which the application classes were loaded. */
+ public static final URI CLASS_CONTAINER = getClassContainer();
+
+ /**
+ * Running from normal jar archive rather than class files? .
+ * When running directly from .class files, we are in development mode and
+ * want to have very short cycles, data is retrieved from local files.
+ * When running from .jar archive, we are in standard mode whereby most data
+ * is meant to be retrieved from the .jar archive itself.
+ */
+ public static final boolean RUNNING_FROM_JAR = runningFromJar();
+
+ /** Containing jar file, if any. */
+ public static final JarFile JAR_FILE = RUNNING_FROM_JAR ? getJarFile() : null;
+
+ /** Time of last modification of jar file, if any. */
+ public static final FileTime JAR_TIME = RUNNING_FROM_JAR ? getJarTime() : null;
+
+ /** The uri where resource data is stored. */
+ public static final URI RES_URI = RUNNING_FROM_JAR
+ ? toURI(
+ WellKnowns.class.getClassLoader().getResource("res"))
+ : Paths.get("res")
+ .toUri();
+
+ /** The folder where Tesseract OCR material is stored. */
+ public static final File OCR_FOLDER = getOcrFolder();
+
+ //-------------// read-write area
+ // USER CONFIG // Configuration files the user can edit on his own
+ //-------------//
+ //
+ /** The config folder where global configuration data is stored. */
+ public static final File CONFIG_FOLDER = RUNNING_FROM_JAR
+ ? getConfigFolder()
+ : new File("config");
+
+ /** The folder where plugin scripts are found. */
+ public static final File PLUGINS_FOLDER = new File(
+ CONFIG_FOLDER,
+ "plugins");
+
+ //-----------// read-write area
+ // USER DATA // User-specific data, except configuration stuff
+ //-----------//
+ //
+ /** Base folder for data */
+ public static final File DATA_FOLDER = RUNNING_FROM_JAR ? getDataFolder()
+ : new File("data");
+
+ /**
+ * The folder where documentations files are installed.
+ * Installation takes place when .jar is run for the first time
+ */
+ public static final File DOC_FOLDER = new File(DATA_FOLDER, "www");
+
+ /**
+ * The folder where examples files are installed.
+ * Installation takes place when .jar is run for the first time
+ */
+ public static final File EXAMPLES_FOLDER = new File(
+ DATA_FOLDER,
+ "examples");
+
+ /** The folder where temporary data can be stored. */
+ public static final File TEMP_FOLDER = new File(DATA_FOLDER, "temp");
+
+ /** The folder where evaluation data is stored. */
+ public static final File EVAL_FOLDER = new File(DATA_FOLDER, "eval");
+
+ /** The folder where training material is stored. */
+ public static final File TRAIN_FOLDER = new File(DATA_FOLDER, "train");
+
+ /** The folder where symbols information is stored. */
+ public static final File SYMBOLS_FOLDER = new File(TRAIN_FOLDER, "symbols");
+
+ /** The default folder where benches data is stored. */
+ public static final File DEFAULT_BENCHES_FOLDER = new File(
+ DATA_FOLDER,
+ "benches");
+
+ /** The default folder where PDF data is stored. */
+ public static final File DEFAULT_PRINT_FOLDER = new File(
+ DATA_FOLDER,
+ "print");
+
+ /** The default folder where scripts data is stored. */
+ public static final File DEFAULT_SCRIPTS_FOLDER = new File(
+ DATA_FOLDER,
+ "scripts");
+
+ /** The default folder where scores data is stored. */
+ public static final File DEFAULT_SCORES_FOLDER = new File(
+ DATA_FOLDER,
+ "scores");
+
+ static {
+ /** Logging configuration. */
+ LogUtil.initialize(CONFIG_FOLDER, TEMP_FOLDER);
+ /** Log declared data (debug). */
+ logDeclaredData();
+ }
+
+ static {
+ /** Disable DirecDraw by default. */
+ disableDirectDraw();
+ }
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //------------//
+ // WellKnowns // Not meant to be instantiated
+ //------------//
+ private WellKnowns ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //--------------//
+ // ensureLoaded //
+ //--------------//
+ /**
+ * Make sure this class is loaded.
+ */
+ public static void ensureLoaded ()
+ {
+ }
+
+ //
+ //-------------------//
+ // disableDirectDraw //
+ //-------------------//
+ private static void disableDirectDraw ()
+ {
+ // See http://performance.netbeans.org/howto/jvmswitches/
+ // -Dsun.java2d.d3d=false
+ // this switch disables DirectDraw and may solve performance problems
+ // with some HW configurations.
+ final String KEY = "sun.java2d.d3d";
+
+ // Respect user setting if any
+ if (System.getProperty(KEY) == null) {
+ System.setProperty(KEY, "false");
+ }
+ }
+
+ //-------------------//
+ // getClassContainer //
+ //-------------------//
+ private static URI getClassContainer ()
+ {
+ return toURI(
+ WellKnowns.class.getProtectionDomain().getCodeSource().getLocation());
+ }
+
+ //-----------------//
+ // getConfigFolder //
+ //-----------------//
+ private static File getConfigFolder ()
+ {
+ if (WINDOWS) {
+ String appdata = System.getenv("APPDATA");
+
+ if (appdata != null) {
+ return new File(appdata + TOOL_PREFIX + "/config");
+ }
+
+ throw new RuntimeException(
+ "APPDATA environment variable is not set");
+ } else if (MAC_OS_X) {
+ String config = System.getenv("XDG_CONFIG_HOME");
+
+ if (config != null) {
+ return new File(config + System.getProperty("user.dir"));
+ }
+
+ String home = System.getenv("HOME");
+
+ if (home != null) {
+ return new File(System.getProperty("user.dir"));
+ }
+
+ throw new RuntimeException("HOME environment variable is not set");
+ } else if (LINUX) {
+ String config = System.getenv("XDG_CONFIG_HOME");
+
+ if (config != null) {
+ return new File(config + TOOL_PREFIX);
+ }
+
+ String home = System.getenv("HOME");
+
+ if (home != null) {
+ return new File(home + "/.config" + TOOL_PREFIX);
+ }
+
+ throw new RuntimeException("HOME environment variable is not set");
+ } else {
+ throw new RuntimeException("Platform unknown");
+ }
+ }
+
+ //---------------//
+ // getDataFolder //
+ //---------------//
+ private static File getDataFolder ()
+ {
+ if (WINDOWS) {
+ String appdata = System.getenv("APPDATA");
+
+ if (appdata != null) {
+ return new File(appdata + TOOL_PREFIX + "/data");
+ }
+
+ throw new RuntimeException(
+ "APPDATA environment variable is not set");
+ } else if (MAC_OS_X) {
+ String data = System.getenv("XDG_DATA_HOME");
+
+ if (data != null) {
+ return new File(System.getProperty("user.dir"));
+ }
+
+ String home = System.getenv("HOME");
+
+ if (home != null) {
+ return new File(System.getProperty("user.dir"));
+ }
+
+ throw new RuntimeException("HOME environment variable is not set");
+ } else if (LINUX) {
+ String data = System.getenv("XDG_DATA_HOME");
+
+ if (data != null) {
+ return new File(data + TOOL_PREFIX);
+ }
+
+ String home = System.getenv("HOME");
+
+ if (home != null) {
+ return new File(home + "/.local/share" + TOOL_PREFIX);
+ }
+
+ throw new RuntimeException("HOME environment variable is not set");
+ } else {
+ throw new RuntimeException("Platform unknown");
+ }
+ }
+
+ //-----------------//
+ // getFileEncoding //
+ //-----------------//
+ private static String getFileEncoding ()
+ {
+ final String ENCODING_KEY = "file.encoding";
+ final String ENCODING_VALUE = "UTF-8";
+
+ System.setProperty(ENCODING_KEY, ENCODING_VALUE);
+
+ return ENCODING_VALUE;
+ }
+
+ //------------//
+ // getJarFile //
+ //------------//
+ private static JarFile getJarFile ()
+ {
+ try {
+ String rn = WellKnowns.class.getName()
+ .replace('.', '/')
+ + ".class";
+ Enumeration en = WellKnowns.class.getClassLoader()
+ .getResources(rn);
+
+ if (en.hasMoreElements()) {
+ URL url = en.nextElement();
+
+ // url = jar:http://audiveris.kenai.com/jnlp/audiveris.jar!/omr/WellKnowns.class
+ JarURLConnection urlcon = (JarURLConnection) (url.openConnection());
+ JarFile jarFile = urlcon.getJarFile();
+
+ return jarFile;
+ }
+ } catch (Exception ex) {
+ System.out.print("Error getting jar file " + ex);
+ }
+
+ return null;
+ }
+
+ //------------//
+ // getJarTime //
+ //------------//
+ private static FileTime getJarTime ()
+ {
+ long millis = JAR_FILE.getEntry("META-INF/MANIFEST.MF")
+ .getTime();
+
+ return FileTime.fromMillis(millis);
+ }
+
+ //--------------//
+ // getOcrFolder //
+ //--------------//
+ private static File getOcrFolder ()
+ {
+ // First, try to use TESSDATA_PREFIX environment variable
+ // which might denote a Tesseract installation
+ final String TESSDATA_PREFIX = "TESSDATA_PREFIX";
+ final String tessPrefix = System.getenv(TESSDATA_PREFIX);
+
+ if (tessPrefix != null) {
+ File dir = new File(tessPrefix);
+
+ if (dir.isDirectory()) {
+ return dir;
+ }
+ }
+
+ // Fallback to default directory
+ if (LINUX) {
+ return new File("/usr/share/tesseract-ocr");
+ } else if (WINDOWS) {
+ final String pf32 = OS_ARCH.equals("x86") ? "ProgramFiles"
+ : "ProgramFiles(x86)";
+
+ return new File(new File(System.getenv(pf32)), "tesseract-ocr");
+ } else {
+ throw new InstallationException("Tesseract-OCR is not installed");
+ }
+ }
+
+ //-----------------//
+ // logDeclaredData //
+ //-----------------//
+ private static void logDeclaredData ()
+ {
+ final Logger logger = LoggerFactory.getLogger(WellKnowns.class);
+
+ // To check updates
+ ///logger.info("Token #{}", 2);
+ if (!RUNNING_FROM_JAR) {
+ // Just to remind the developer we are NOT running in normal mode
+ logger.info("[Not running from jar]");
+ } else {
+ // Debug, to identify this jar
+ logger.debug(
+ "JarTime: {} JarFile: {}",
+ JAR_TIME,
+ JAR_FILE.getName());
+ }
+
+ if (logger.isDebugEnabled()) {
+ for (Field field : WellKnowns.class.getDeclaredFields()) {
+ try {
+ logger.debug("{}= {}", field.getName(), field.get(null));
+ } catch (IllegalAccessException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+ }
+
+ //----------------//
+ // runningFromJar //
+ //----------------//
+ private static boolean runningFromJar ()
+ {
+ return CLASS_CONTAINER.toString()
+ .toLowerCase()
+ .endsWith(".jar");
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-----------------------//
+ // InstallationException //
+ //-----------------------//
+ /**
+ * Exception used to signal an installation error.
+ */
+ public static class InstallationException
+ extends RuntimeException
+ {
+ //~ Constructors -------------------------------------------------------
+
+ public InstallationException (String message)
+ {
+ super(message);
+ }
+ }
+}
diff --git a/src/main/omr/action/ActionDescriptor.java b/src/main/omr/action/ActionDescriptor.java
new file mode 100644
index 0000000..4ff63d7
--- /dev/null
+++ b/src/main/omr/action/ActionDescriptor.java
@@ -0,0 +1,115 @@
+//----------------------------------------------------------------------------//
+// //
+// A c t i o n D e s c r i p t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.action;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Class {@code ActionDescriptor} gathers parameters related to an action
+ * from the User Interface point of view
+ *
+ * @author Hervé Bitteur
+ */
+@XmlAccessorType(XmlAccessType.NONE)
+@XmlRootElement(name = "action")
+public class ActionDescriptor
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /** Class name */
+ @XmlAttribute(name = "class")
+ public String className;
+
+ /** Name of method within class */
+ @XmlAttribute(name = "method")
+ public String methodName;
+
+ /** Which UI domain (menu) should host this action */
+ @XmlAttribute(name = "domain")
+ public String domain;
+
+ /**
+ * Which UI section should host this action.
+ * Any value is OK, but items with the same section value will be gathered
+ * together in the menu, while different sections will be separated by a
+ * menu separator
+ */
+ @XmlAttribute(name = "section")
+ public Integer section;
+
+ /**
+ * Which kind of menu item should be generated for this action,
+ * default is JMenuItem
+ */
+ @XmlAttribute(name = "item")
+ public String itemClassName;
+
+ /**
+ * Which kind of (toolbar) button should be generated for this action,
+ * default is null
+ */
+ @XmlAttribute(name = "button")
+ public String buttonClassName;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //------------------//
+ // ActionDescriptor //
+ //------------------//
+ /**
+ * To force instantiation through JAXB unmarshalling only.
+ */
+ private ActionDescriptor ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Report a one-line information on this descriptor
+ */
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{action");
+ sb.append(" class:").
+ append(className).
+ append(" method:").
+ append(methodName);
+
+ sb.append(" domain:").
+ append(domain);
+ sb.append(" section:").
+ append(section);
+
+ if (itemClassName != null) {
+ sb.append(" item:").
+ append(itemClassName);
+ }
+
+ if (buttonClassName != null) {
+ sb.append(" button:").
+ append(buttonClassName);
+ }
+
+ sb.append("}");
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/omr/action/ActionManager.java b/src/main/omr/action/ActionManager.java
new file mode 100644
index 0000000..304639e
--- /dev/null
+++ b/src/main/omr/action/ActionManager.java
@@ -0,0 +1,405 @@
+//----------------------------------------------------------------------------//
+// //
+// A c t i o n M a n a g e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.action;
+
+import omr.WellKnowns;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import omr.ui.MainGui;
+import omr.ui.util.SeparableMenu;
+import omr.ui.util.UIUtil;
+
+import omr.util.UriUtil;
+
+import org.jdesktop.application.ApplicationAction;
+import org.jdesktop.application.ResourceMap;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.AbstractButton;
+import javax.swing.Action;
+import javax.swing.ActionMap;
+import javax.swing.Box;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JToolBar;
+import javax.xml.bind.JAXBException;
+
+/**
+ * Class {@code ActionManager} handles the instantiation and dressing
+ * of actions, their organization in the menus and the tool bar, and
+ * their enabling.
+ *
+ * @author Hervé Bitteur
+ */
+public class ActionManager
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(ActionManager.class);
+
+ /** Class loader */
+ private static final ClassLoader classLoader = ActionManager.class.
+ getClassLoader();
+
+ /** Singleton */
+ private static volatile ActionManager INSTANCE;
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** The map of all menus, so that we can directly provide some. */
+ private final Map menuMap = new HashMap<>();
+
+ /** Collection of actions enabled only when a sheet is selected. */
+ private final Collection sheetDependentActions = new ArrayList<>();
+
+ /** Collection of actions enabled only when current score is available. */
+ private final Collection scoreDependentActions = new ArrayList<>();
+
+ /** The tool bar that hosts some actions. */
+ private final JToolBar toolBar = new JToolBar();
+
+ /** The menu bar for all actions. */
+ private final JMenuBar menuBar = new JMenuBar();
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //---------------//
+ // ActionManager //
+ //---------------//
+ /**
+ * Meant to be instantiated at most once.
+ */
+ private ActionManager ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-------------//
+ // getInstance //
+ //-------------//
+ /**
+ * Report the single action manager instance.
+ *
+ * @return the unique instance of this class
+ */
+ public static ActionManager getInstance ()
+ {
+ if (INSTANCE == null) {
+ INSTANCE = new ActionManager();
+ }
+
+ return INSTANCE;
+ }
+
+ //-------------------//
+ // getActionInstance //
+ //-------------------//
+ /**
+ * Retrieve an action knowing its methodName.
+ *
+ * @param instance the instance of the hosting class
+ * @param methodName the method name
+ * @return the action found, or null if none
+ */
+ public ApplicationAction getActionInstance (Object instance,
+ String methodName)
+ {
+ ActionMap actionMap = MainGui.getInstance().getContext().getActionMap(
+ instance);
+
+ return (ApplicationAction) actionMap.get(methodName);
+ }
+
+ //---------//
+ // getMenu //
+ //---------//
+ /**
+ * Report the menu built for a given key.
+ *
+ * @param key the given menu key
+ * @return the related menu
+ */
+ public JMenu getMenu (String key)
+ {
+ return menuMap.get(key);
+ }
+
+ //------------//
+ // getMenuBar //
+ //------------//
+ /**
+ * Report the bar containing all generated pull-down menus.
+ *
+ * @return the menu bar
+ */
+ public JMenuBar getMenuBar ()
+ {
+ return menuBar;
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report a describing name.
+ *
+ * @return a describing name
+ */
+ public String getName ()
+ {
+ return "ActionManager";
+ }
+
+ //------------//
+ // getToolBar //
+ //------------//
+ /**
+ * Report the tool bar containing all generated buttons.
+ *
+ * @return the tool bar
+ */
+ public JToolBar getToolBar ()
+ {
+ return toolBar;
+ }
+
+ //------------//
+ // injectMenu //
+ //------------//
+ /**
+ * Insert a predefined menu, either partly or fully built.
+ *
+ * @param key the menu unique name
+ * @param menu the menu to inject
+ */
+ public void injectMenu (String key,
+ JMenu menu)
+ {
+ menuMap.put(key, menu);
+ }
+
+ //--------------------//
+ // loadAllDescriptors //
+ //--------------------//
+ /**
+ * Load all descriptors as found in system and user configuration
+ * files.
+ */
+ public void loadAllDescriptors ()
+ {
+ // Load classes first for system actions, then for user actions
+ URI[] uris = new URI[]{
+ UriUtil.toURI(WellKnowns.RES_URI, "system-actions.xml"),
+ new File(WellKnowns.CONFIG_FOLDER, "user-actions.xml").toURI().normalize()};
+
+ for (int i = 0; i < uris.length; i++) {
+ URI uri = uris[i];
+ try {
+ URL url = uri.toURL();
+ InputStream input = url.openStream();
+ Actions.loadActionsFrom(input);
+ } catch (IOException ex) {
+ // Item does not exist
+ if (i == 0) {
+ // Only the first item (system) is mandatory
+ logger.error("Mandatory file not found {}", uri);
+ }
+ } catch (JAXBException ex) {
+ logger.warn("Error loading actions from " + uri, ex);
+ }
+ }
+ }
+
+ //--------------------//
+ // registerAllActions //
+ //--------------------//
+ /**
+ * Register all actions as listed in the descriptor files, and
+ * organize them according to the various domains defined.
+ * There is one pull-down menu generated for each domain found.
+ */
+ public void registerAllActions ()
+ {
+ // Insert an initial separator, to let user easily grab the toolBar
+ toolBar.addSeparator();
+
+ for (String domain : Actions.getDomainNames()) {
+ // Create dedicated menu for this range, if not already existing
+ JMenu menu = menuMap.get(domain);
+
+ if (menu == null) {
+ logger.debug("Creating menu:{}", domain);
+ menu = new SeparableMenu(domain);
+ menuMap.put(domain, menu);
+ } else {
+ logger.debug("Augmenting menu:{}", domain);
+ }
+
+ // Proper menu decoration
+ ResourceMap resource = MainGui.getInstance().getContext().
+ getResourceMap(Actions.class);
+ menu.setText(domain); // As default
+ menu.setName(domain);
+
+ // Register all actions in the given domain
+ registerDomainActions(domain, menu);
+ resource.injectComponents(menu);
+
+ toolBar.addSeparator();
+
+ // Smart insertion of the menu into the menu bar
+ if (menu.getItemCount() > 0) {
+ if (domain.equalsIgnoreCase("help")) {
+ menuBar.add(Box.createHorizontalStrut(50));
+ }
+
+ SeparableMenu.trimSeparator(menu); // No separator at end
+ menuBar.add(menu);
+ }
+ }
+ }
+
+ //----------------//
+ // registerAction //
+ //----------------//
+ /**
+ * Allocate and dress an instance of the provided class, then
+ * register the action in the UI structure (menus and buttons)
+ * according to the action descriptor parameters.
+ *
+ * @param action the provided action class
+ * @return the registered and decorated instance of the action class
+ */
+ @SuppressWarnings("unchecked")
+ private ApplicationAction registerAction (ActionDescriptor desc)
+ {
+ ///logger.info("registerAction. " + desc);
+ ApplicationAction action = null;
+
+ try {
+ // Retrieve proper class instance
+ Class> classe = classLoader.loadClass(desc.className);
+ Object instance = null;
+
+ // Reuse existing instance through a 'getInstance()' method if any
+ try {
+ Method getInstance = classe.getDeclaredMethod(
+ "getInstance",
+ (Class[]) null);
+
+ if (Modifier.isStatic(getInstance.getModifiers())) {
+ instance = getInstance.invoke(null);
+ }
+ } catch (NoSuchMethodException ignored) {
+ }
+
+ if (instance == null) {
+ // Fall back to allocate a new class instance
+ ///logger.warn("instantiating instance of " + classe);
+ instance = classe.newInstance();
+ }
+
+ // Retrieve the action instance
+ action = getActionInstance(instance, desc.methodName);
+
+ if (action != null) {
+ // Insertion of a button on Tool Bar?
+ if (desc.buttonClassName != null) {
+ Class extends AbstractButton> buttonClass =
+ (Class extends AbstractButton>) classLoader.
+ loadClass(desc.buttonClassName);
+ AbstractButton button = buttonClass.newInstance();
+ button.setAction(action);
+ toolBar.add(button);
+ button.setBorder(UIUtil.getToolBorder());
+ button.setText("");
+ }
+ } else {
+ logger.error("Unknown action {} in class {}",
+ desc.methodName, desc.className);
+ }
+ } catch (ClassNotFoundException | SecurityException |
+ IllegalAccessException | IllegalArgumentException |
+ InvocationTargetException | InstantiationException ex) {
+ logger.warn("Error while registering " + desc, ex);
+ }
+
+ return action;
+ }
+
+ //-----------------------//
+ // registerDomainActions //
+ //-----------------------//
+ @SuppressWarnings("unchecked")
+ private void registerDomainActions (String domain,
+ JMenu menu)
+ {
+ // Create all type sections for this menu
+ for (int section : Actions.getSections()) {
+ logger.debug("Starting section: {}", section);
+
+ // Use a separator between sections
+ menu.addSeparator();
+
+ for (ActionDescriptor desc : Actions.getAllDescriptors()) {
+ if (desc.domain.equalsIgnoreCase(domain)
+ && (desc.section == section)) {
+ logger.debug("Registering {}", desc);
+
+ try {
+ Class extends JMenuItem> itemClass;
+
+ if (desc.itemClassName != null) {
+ itemClass = (Class extends JMenuItem>) classLoader.
+ loadClass(
+ desc.itemClassName);
+ } else {
+ itemClass = JMenuItem.class;
+ }
+
+ JMenuItem item = itemClass.newInstance();
+ item.setText(desc.methodName);
+
+ ApplicationAction action = registerAction(desc);
+
+ if (action != null) {
+ action.setSelected(action.isSelected());
+ item.setAction(action);
+ menu.add(item);
+ } else {
+ logger.warn("Could not register {}", desc);
+ }
+ } catch (ClassNotFoundException | InstantiationException |
+ IllegalAccessException ex) {
+ logger.warn("Error with " + desc.itemClassName, ex);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/omr/action/Actions.java b/src/main/omr/action/Actions.java
new file mode 100644
index 0000000..0928dcb
--- /dev/null
+++ b/src/main/omr/action/Actions.java
@@ -0,0 +1,211 @@
+//----------------------------------------------------------------------------//
+// //
+// A c t i o n s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.action;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Class {@code Actions} handles all actions descriptors.
+ *
+ * @author Hervé Bitteur
+ */
+@XmlAccessorType(XmlAccessType.NONE)
+@XmlRootElement(name = "actions")
+public class Actions
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Actions.class);
+
+ /** Context for JAXB unmarshalling */
+ private static volatile JAXBContext jaxbContext;
+
+ /** The collection of all actions loaded so far */
+ private static final Set allDescriptors = new LinkedHashSet<>();
+
+ //~ Enumerations -----------------------------------------------------------
+ /**
+ * Predefined list of domain names.
+ * Through the action list files, the user will be able to add new domain
+ * names.
+ * This classification is mainly used to define the related pull-down menus.
+ */
+ public static enum Domain
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ /** Domain of file actions */
+ FILE,
+ /** Domain of individual steps */
+ STEP,
+ /** Domain of score actions */
+ SCORE,
+ /** Domain of MIDI features */
+ MIDI,
+ /** Domain of various view features */
+ VIEW,
+ /** Domain of utilities */
+ TOOL,
+ /** Domain of plugins */
+ PLUGIN,
+ /** Domain of help information */
+ HELP;
+ }
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** Collection of descriptors loaded by unmarshalling one file. */
+ @XmlElement(name = "action")
+ private List descriptors = new ArrayList<>();
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //---------//
+ // Actions //
+ //---------//
+ /** Not meant to be instantiated */
+ private Actions ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //-------------------//
+ // getAllDescriptors //
+ //-------------------//
+ /**
+ * Report the collection of descriptors loaded so far.
+ * @return all the loaded action descriptors
+ */
+ public static Set getAllDescriptors ()
+ {
+ return allDescriptors;
+ }
+
+ //----------------//
+ // getDomainNames //
+ //----------------//
+ /**
+ * Report the whole collection of domain names, starting with the
+ * predefined ones.
+ * @return the collection of domain names
+ */
+ public static Set getDomainNames ()
+ {
+ Set names = new LinkedHashSet<>();
+
+ // Predefined ones, except HELP
+ for (Domain domain : Domain.values()) {
+ if (domain != Domain.HELP) {
+ names.add(domain.name());
+ }
+ }
+
+ // User-specified ones, except HELP
+ for (ActionDescriptor desc : allDescriptors) {
+ if (!desc.domain.equalsIgnoreCase(Domain.HELP.toString())) {
+ names.add(desc.domain);
+ }
+ }
+
+ // Add HELP as the very last one
+ names.add(Domain.HELP.name());
+
+ return names;
+ }
+
+ //-------------//
+ // getSections //
+ //-------------//
+ /**
+ * Report the whole collection of sections, the predefined ones
+ * and the added ones.
+ * @return the collection of sections
+ */
+ public static SortedSet getSections ()
+ {
+ SortedSet sections = new TreeSet<>();
+
+ for (ActionDescriptor desc : allDescriptors) {
+ sections.add(desc.section);
+ }
+
+ return sections;
+ }
+
+ //-----------------//
+ // loadActionsFrom //
+ //-----------------//
+ /**
+ * Unmarshal the provided XML stream to allocate the corresponding
+ * collection of action descriptors.
+ *
+ * @param in the input stream that contains the collection of action
+ * descriptors in XML format. The stream is not closed by this method
+ * @throws javax.xml.bind.JAXBException
+ */
+ public static void loadActionsFrom (InputStream in)
+ throws JAXBException
+ {
+ if (jaxbContext == null) {
+ jaxbContext = JAXBContext.newInstance(Actions.class);
+ }
+
+ Unmarshaller um = jaxbContext.createUnmarshaller();
+ Actions actions = (Actions) um.unmarshal(in);
+
+ for (ActionDescriptor desc : actions.descriptors) {
+ logger.debug("Descriptor unmarshalled {}", desc);
+ }
+
+ // Verify that all actions have a domain and a section assigned
+ for (Iterator it = actions.descriptors.iterator();
+ it.hasNext();) {
+ ActionDescriptor desc = it.next();
+
+ if (desc.domain == null) {
+ logger.warn("No domain specified for {}", desc);
+ it.remove();
+
+ continue;
+ }
+
+ if (desc.section == null) {
+ logger.warn("No section specified for {}", desc);
+ it.remove();
+
+ continue;
+ }
+ }
+
+ allDescriptors.addAll(actions.descriptors);
+ }
+}
diff --git a/src/main/omr/action/package.html b/src/main/omr/action/package.html
new file mode 100644
index 0000000..e35ea01
--- /dev/null
+++ b/src/main/omr/action/package.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Package omr.action
+
+
+
+
+ Package dedicated to the implementation of user actions, both
+ system (predefined) or user (custom) actions
+
+
+
+
diff --git a/src/main/omr/action/resources/Actions.properties b/src/main/omr/action/resources/Actions.properties
new file mode 100644
index 0000000..784a946
--- /dev/null
+++ b/src/main/omr/action/resources/Actions.properties
@@ -0,0 +1,36 @@
+# ---------------------------------------------------------------------------- #
+# #
+# A c t i o n s . p r o p e r t i e s #
+# #
+# ---------------------------------------------------------------------------- #
+
+# This file gathers resources to be injected in the Actions class
+#
+# This is the generic version
+
+FILE.text = File
+FILE.toolTipText = Management of image files
+
+STEP.text = Step
+STEP.toolTipText = Sequence of steps
+
+SCORE.text = Score
+SCORE.toolTipText = Management of scores
+
+MIDI.text = Midi
+MIDI.toolTipText = Management of Midi features
+
+VIEW.text = Views
+VIEW.toolTipText = View Parameters
+
+TOOL.text = Tools
+TOOL.toolTipText = Various tools
+
+PLUGIN.text = Plugins
+PLUGIN.toolTipText = Declared plugins
+
+HELP.text = Help
+HELP.toolTipText = Help information
+
+DEBUG.text = Debug
+DEBUG.toolTipText = Miscellaneous debug actions
diff --git a/src/main/omr/action/resources/Actions_fr.properties b/src/main/omr/action/resources/Actions_fr.properties
new file mode 100644
index 0000000..6e65637
--- /dev/null
+++ b/src/main/omr/action/resources/Actions_fr.properties
@@ -0,0 +1,35 @@
+# ---------------------------------------------------------------------------- #
+# #
+# A c t i o n s _ f r . p r o p e r t i e s #
+# #
+# ---------------------------------------------------------------------------- #
+
+# This file gathers resources to be injected in the Actions class
+#
+# This is the FR version
+
+FILE.text = Fichier
+FILE.toolTipText = Gestion des feuilles
+
+STEP.text = Etape
+STEP.toolTipText = S\u00e9quence des \u00e9tapes
+
+SCORE.text = Partition
+SCORE.toolTipText = Gestion des partitions
+
+MIDI.toolTipText = Gestion Midi
+
+VIEW.text = Vues
+VIEW.toolTipText = Param\u00e8tres des vues
+
+TOOL.text = Outils
+TOOL.toolTipText = Outils divers
+
+PLUGIN.text = Plugins
+PLUGIN.toolTipText = Plugins d\u00e9clar\u00e9s
+
+HELP.text = Aide
+HELP.toolTipText = Information d'aide
+
+DEBUG.text = Debug
+DEBUG.toolTipText = Diverses actions de debugging
diff --git a/src/main/omr/check/Check.java b/src/main/omr/check/Check.java
new file mode 100644
index 0000000..0bbdd01
--- /dev/null
+++ b/src/main/omr/check/Check.java
@@ -0,0 +1,353 @@
+//----------------------------------------------------------------------------//
+// //
+// C h e c k //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+import omr.constant.Constant;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class {@code Check} encapsulates the definition of a check,
+ * which can later be used on a whole population of objects.
+ *
+ * The result of using a check on a given object is not recorded in this
+ * class, but into the checked entity itself.
+ *
+ * Checks can be gathered in check suites.
+ *
+ * @param precise type of the objects to be checked
+ *
+ * @author Hervé Bitteur
+ */
+public abstract class Check
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Check.class);
+
+ /**
+ * Indicates a negative result
+ */
+ public static final int RED = -1;
+
+ /**
+ * Indicates a non-concluding result
+ */
+ public static final int ORANGE = 0;
+
+ /**
+ * Indicates a positive result
+ */
+ public static final int GREEN = 1;
+
+ //~ Instance fields --------------------------------------------------------
+ /**
+ * Specifies the FailureResult to be assigned to the Checkable object, if
+ * the result of the check end in the RED range.
+ */
+ private final FailureResult redResult;
+
+ /** Longer description, meant for tips */
+ private final String description;
+
+ /** Short name for this test */
+ private final String name;
+
+ /**
+ * Specifies if values are RED,ORANGE,GREEN (higher is better, covariant =
+ * true) or GREEN,ORANGE,RED (lower is better, covariant = false)
+ */
+ private final boolean covariant;
+
+ /**
+ * Lower bound for ORANGE range. Whatever the value of 'covariant', we must
+ * always have low <= high
+ */
+ private Constant.Double low;
+
+ /**
+ * Higher bound for ORANGE range. Whatever the value of 'covariant', we must
+ * always have low <= high
+ */
+ private Constant.Double high;
+
+ //~ Constructors -----------------------------------------------------------
+ //-------//
+ // Check //
+ //-------//
+ /**
+ * Creates a new Check object.
+ *
+ * @param name short name for this check
+ * @param description longer description
+ * @param low lower bound of orange zone
+ * @param high upper bound of orange zone
+ * @param covariant true if higher is better, false otherwise
+ * @param redResult result code to be assigned when result is RED
+ */
+ protected Check (String name,
+ String description,
+ Constant.Double low,
+ Constant.Double high,
+ boolean covariant,
+ FailureResult redResult)
+ {
+ this.name = name;
+ this.description = description;
+ this.low = low;
+ this.high = high;
+ this.covariant = covariant;
+ this.redResult = redResult;
+
+ verifyRange();
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // getDescription //
+ //----------------//
+ /**
+ * Report the related description
+ *
+ * @return the description assigned to this check
+ */
+ public String getDescription ()
+ {
+ return description;
+ }
+
+ //---------//
+ // getHigh //
+ //---------//
+ /**
+ * Report the higher bound value
+ *
+ * @return the high bound
+ */
+ public double getHigh ()
+ {
+ return high.getValue();
+ }
+
+ //-----------------//
+ // getHighConstant //
+ //-----------------//
+ /**
+ * Report the higher bound constant
+ *
+ * @return the high bound constant
+ */
+ public Constant.Double getHighConstant ()
+ {
+ return high;
+ }
+
+ //--------//
+ // getLow //
+ //--------//
+ /**
+ * Report the lower bound value
+ *
+ * @return the low bound
+ */
+ public double getLow ()
+ {
+ return low.getValue();
+ }
+
+ //----------------//
+ // getLowConstant //
+ //----------------//
+ /**
+ * Report the lower bound constant
+ *
+ * @return the low bound constant
+ */
+ public Constant.Double getLowConstant ()
+ {
+ return low;
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report the related name
+ *
+ * @return the name assigned to this check
+ */
+ public String getName ()
+ {
+ return name;
+ }
+
+ //-------------//
+ // isCovariant //
+ //-------------//
+ /**
+ * Report the covariant flag
+ *
+ * @return the value of covariant flag
+ */
+ public boolean isCovariant ()
+ {
+ return covariant;
+ }
+
+ //------//
+ // pass //
+ //------//
+ /**
+ * Actually run the check on the provided object, and return the result. As
+ * a side-effect, a check that totally fails (RED result) assigns this
+ * failure into the candidate object.
+ *
+ * @param obj the checkable object to be checked
+ * @param result output for the result, or null
+ * @param update true if obj is to be updated with the result
+ *
+ * @return the result composed of the numerical value, plus a flag ({@link
+ * #RED}, {@link #ORANGE}, {@link #GREEN}) that characterizes the result of
+ * passing the check on this object
+ */
+ public CheckResult pass (C obj,
+ CheckResult result,
+ boolean update)
+ {
+ if (result == null) {
+ result = new CheckResult();
+ }
+
+ result.value = getValue(obj);
+
+ if (covariant) {
+ if (result.value < low.getValue()) {
+ if (update) {
+ obj.setResult(redResult);
+ }
+
+ result.flag = RED;
+ } else if (result.value >= high.getValue()) {
+ result.flag = GREEN;
+ } else {
+ result.flag = ORANGE;
+ }
+ } else {
+ if (result.value <= low.getValue()) {
+ result.flag = GREEN;
+ } else if (result.value > high.getValue()) {
+ if (update) {
+ obj.setResult(redResult);
+ }
+
+ result.flag = RED;
+ } else {
+ result.flag = ORANGE;
+ }
+ }
+
+ return result;
+ }
+
+ //------------//
+ // setLowHigh //
+ //------------//
+ /**
+ * Allows to set the pair of low and high value. They are set in one shot to
+ * allow the sanity check of 'low' less than or equal to 'high'
+ *
+ * @param low the new low value
+ * @param high the new high value
+ */
+ public void setLowHigh (Constant.Double low,
+ Constant.Double high)
+ {
+ this.low = low;
+ this.high = high;
+ verifyRange();
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * report a readable description of this check
+ */
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("{Check ")
+ .append(name);
+ sb.append(" Covariant:")
+ .append(covariant);
+ sb.append(" Low:")
+ .append(low);
+ sb.append(" High:")
+ .append(high);
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ //----------//
+ // getValue //
+ //----------//
+ /**
+ * Method to be provided by any concrete subclass, in order to retrieve the
+ * proper data value from the given object passed as a parameter.
+ *
+ * @param obj the object to be checked
+ *
+ * @return the data value relevant for the check
+ */
+ protected abstract double getValue (C obj);
+
+ //-------------//
+ // verifyRange //
+ //-------------//
+ private void verifyRange ()
+ {
+ if (low.getValue() > high.getValue()) {
+ logger.error(
+ "Illegal low {} high {} range for {}",
+ low.getValue(), high.getValue(), this);
+ }
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-------//
+ // Grade //
+ //-------//
+ /**
+ * A subclass of Constant.Double, meant to store a check result grade.
+ */
+ public static class Grade
+ extends Constant.Double
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Specific constructor, where 'unit' and 'name' are assigned later
+ *
+ * @param defaultValue the (double) default value
+ * @param description the semantic of the constant
+ */
+ public Grade (double defaultValue,
+ java.lang.String description)
+ {
+ super("Grade", defaultValue, description);
+ }
+ }
+}
diff --git a/src/main/omr/check/CheckBoard.java b/src/main/omr/check/CheckBoard.java
new file mode 100644
index 0000000..db31100
--- /dev/null
+++ b/src/main/omr/check/CheckBoard.java
@@ -0,0 +1,163 @@
+//----------------------------------------------------------------------------//
+// //
+// C h e c k B o a r d //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import omr.selection.MouseMovement;
+import omr.selection.SelectionService;
+import omr.selection.UserEvent;
+
+import omr.ui.Board;
+
+import com.jgoodies.forms.builder.PanelBuilder;
+import com.jgoodies.forms.layout.CellConstraints;
+import com.jgoodies.forms.layout.FormLayout;
+
+/**
+ * Class {@code CheckBoard} defines a board dedicated to the display of
+ * check result information.
+ *
+ * @param The {@link Checkable} entity type to be checked
+ *
+ * @author Hervé Bitteur
+ */
+public class CheckBoard
+ extends Board
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(CheckBoard.class);
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** For display of check suite results */
+ private final CheckPanel checkPanel;
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //------------//
+ // CheckBoard //
+ //------------//
+ /**
+ * Create a Check Board.
+ *
+ * @param name the name of the check
+ * @param suite the check suite to be used
+ * @param selectionService which selection service to use
+ * @param eventList which event classes to expect
+ */
+ public CheckBoard (String name,
+ CheckSuite suite,
+ SelectionService selectionService,
+ Class[] eventList)
+ {
+ super(name,
+ Board.CHECK.position,
+ selectionService,
+ eventList,
+ false, // Dump
+ false); // Selected
+ checkPanel = new CheckPanel<>(suite);
+
+ if (suite != null) {
+ defineLayout(suite.getName());
+ }
+
+ // define default content
+ tellObject(null);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //---------//
+ // onEvent //
+ //---------//
+ /**
+ * Call-back triggered when C Selection has been modified.
+ *
+ * @param event the Event to perform check upon its data
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void onEvent (UserEvent event)
+ {
+ try {
+ // Ignore RELEASING
+ if (event.movement == MouseMovement.RELEASING) {
+ return;
+ }
+
+ tellObject((C) event.getData()); // Compiler warning
+ } catch (Exception ex) {
+ logger.warn(getClass().getName() + " onEvent error", ex);
+ }
+ }
+
+ //------------//
+ // applySuite //
+ //------------//
+ /**
+ * Assign a (new) suite to the check board and apply it to the
+ * provided object.
+ *
+ * @param suite the (new) check suite to be used
+ * @param object the object to apply the checks suite on
+ */
+ public synchronized void applySuite (CheckSuite suite,
+ C object)
+ {
+ final boolean toBuild = checkPanel.getComponent() == null;
+ checkPanel.setSuite(suite);
+
+ if (toBuild) {
+ defineLayout(suite.getName());
+ }
+
+ tellObject(object);
+ }
+
+ //------------//
+ // tellObject //
+ //------------//
+ /**
+ * Render the result of checking the given object.
+ *
+ * @param object the object whose check result is to be displayed
+ */
+ protected final void tellObject (C object)
+ {
+ if (object == null) {
+ setVisible(false);
+ } else {
+ setVisible(true);
+ checkPanel.passForm(object);
+ }
+ }
+
+ //--------------//
+ // defineLayout //
+ //--------------//
+ private void defineLayout (String name)
+ {
+ FormLayout layout = new FormLayout("pref", "pref");
+ PanelBuilder builder = new PanelBuilder(layout, getBody());
+ builder.setDefaultDialogBorder();
+
+ CellConstraints cst = new CellConstraints();
+
+ int r = 1; // --------------------------------
+ builder.add(checkPanel.getComponent(), cst.xy(1, r));
+ }
+}
diff --git a/src/main/omr/check/CheckPanel.java b/src/main/omr/check/CheckPanel.java
new file mode 100644
index 0000000..968e680
--- /dev/null
+++ b/src/main/omr/check/CheckPanel.java
@@ -0,0 +1,550 @@
+//----------------------------------------------------------------------------//
+// //
+// C h e c k P a n e l //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+import omr.constant.Constant;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import omr.ui.util.Panel;
+
+import com.jgoodies.forms.builder.PanelBuilder;
+import com.jgoodies.forms.layout.CellConstraints;
+import com.jgoodies.forms.layout.FormLayout;
+
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Scanner;
+
+import javax.swing.AbstractAction;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+
+/**
+ * Class {@code CheckPanel} handles a panel to display the results of a
+ * check suite.
+ *
+ * @param the subtype of Checkable-compatible objects used in the
+ * homogeneous collection of checks of the suite
+ *
+ * @author Hervé Bitteur
+ */
+public class CheckPanel
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(CheckPanel.class);
+
+ // Colors
+ private static final Color GREEN_COLOR = new Color(100, 150, 0);
+
+ private static final Color ORANGE_COLOR = new Color(255, 150, 0);
+
+ private static final Color RED_COLOR = new Color(255, 0, 0);
+
+ // Sizes
+ private static final String LINE_GAP = "1dlu";
+
+ private static final String COLUMN_GAP = "1dlu";
+
+ private static final int FIELD_WIDTH = 4;
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** The related check suite (the model) */
+ private CheckSuite suite;
+
+ /** The swing component that includes all the fields */
+ private Panel component;
+
+ /** The field for global result */
+ private JTextField globalField;
+
+ /** Matrix of all value fields */
+ private JTextField[][] values;
+
+ /** Matrix of all bound fields */
+ private JTextField[][] bounds;
+
+ /** Last object checked */
+ private C object;
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //------------//
+ // CheckPanel //
+ //------------//
+ /**
+ * Create a check panel for a given suite.
+ *
+ * @param suite the suite whose results are to be displayed
+ */
+ public CheckPanel (CheckSuite suite)
+ {
+ // Global field (for global result)
+ globalField = new JTextField(FIELD_WIDTH);
+ globalField.setEditable(false);
+ globalField.setHorizontalAlignment(JTextField.CENTER);
+
+ setSuite(suite);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //--------------//
+ // getComponent //
+ //--------------//
+ /**
+ * Report the UI component.
+ *
+ * @return the concrete component
+ */
+ public JComponent getComponent ()
+ {
+ return component;
+ }
+
+ //----------//
+ // passForm //
+ //----------//
+ /**
+ * Pass the whole suite on the provided checkable object, and
+ * display the results.
+ *
+ * @param object the object to be checked
+ */
+ public void passForm (C object)
+ {
+ // Remember the 'current' object'
+ this.object = object;
+
+ resetValues();
+
+ if (object == null) {
+ return;
+ }
+
+ CheckResult result = new CheckResult();
+ double grade = 0d;
+ boolean failed = false;
+
+ // Fill one row per check
+ for (int index = 0; index < suite.getChecks().size(); index++) {
+ Check check = suite.getChecks().get(index);
+
+ try {
+ // Run this check
+ check.pass(object, result, false);
+ grade += (result.flag * suite.getWeights().get(index));
+
+ // Update proper field to display check result
+ JTextField field;
+
+ switch (result.flag) {
+ case Check.RED:
+ failed = true;
+
+ if (check.isCovariant()) {
+ field = values[index][0];
+ field.setToolTipText("Value is too low");
+ } else {
+ field = values[index][2];
+ field.setToolTipText("Value is too high");
+ }
+
+ field.setForeground(RED_COLOR);
+
+ break;
+
+ case Check.ORANGE:
+ field = values[index][1];
+ field.setToolTipText("Value is acceptable");
+ field.setForeground(ORANGE_COLOR);
+
+ break;
+
+ default:
+ case Check.GREEN:
+
+ if (check.isCovariant()) {
+ field = values[index][2];
+ } else {
+ field = values[index][0];
+ }
+
+ field.setToolTipText("Value is OK");
+ field.setForeground(GREEN_COLOR);
+
+ break;
+ }
+
+ field.setText(textOf(result.value));
+ } catch (Throwable ex) {
+ logger.warn("Failure in check " + check.getName(), ex);
+ failed = true;
+ }
+ }
+
+ // Global suite result
+ if (failed) {
+ globalField.setForeground(RED_COLOR);
+ globalField.setToolTipText("Check has failed!");
+ globalField.setText("Failed");
+ } else {
+ grade /= suite.getTotalWeight();
+
+ if (grade >= suite.getThreshold()) {
+ globalField.setForeground(GREEN_COLOR);
+ globalField.setToolTipText("Check has succeeded!");
+ } else {
+ globalField.setForeground(RED_COLOR);
+ globalField.setToolTipText("Check has failed!");
+ }
+
+ globalField.setText(textOf(grade));
+ }
+ }
+
+ //----------//
+ // setSuite //
+ //----------//
+ /**
+ * Assign a (new) suite to the check pane.
+ *
+ * @param suite the (new) check suite to be used
+ */
+ public final void setSuite (CheckSuite suite)
+ {
+ this.suite = suite;
+
+ if (suite != null) {
+ createValueFields(); // Values
+ createBoundFields(); // Bounds
+ buildComponent(); // Create/update component
+ }
+
+ // Refresh the display
+ if (component != null) {
+ component.validate();
+ component.repaint();
+ }
+ }
+
+ //----------------//
+ // buildComponent //
+ //----------------//
+ private void buildComponent ()
+ {
+ // Either allocate a new Panel or empty the existing one
+ if (component == null) {
+ component = new Panel();
+ component.setNoInsets();
+
+ // Needed to process user input when RETURN/ENTER is pressed
+ component.getInputMap(
+ JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ put(KeyStroke.getKeyStroke("ENTER"), "ParamAction");
+ component.getActionMap().put("ParamAction", new ParamAction());
+ } else {
+ component.removeAll();
+ }
+
+ final int checkNb = suite.getChecks().size();
+ PanelBuilder b = new PanelBuilder(createLayout(checkNb), component);
+ b.setDefaultDialogBorder();
+
+ CellConstraints c = new CellConstraints();
+
+ // Rows
+ int ic = -1; // Check index
+ int r = -1; // Row index
+
+ for (Check check : suite.getChecks()) {
+ ic++;
+ r += 2;
+
+ // Covariance label
+ JLabel covariantLabel;
+
+ if (check.isCovariant()) {
+ covariantLabel = new JLabel(">");
+ covariantLabel.setToolTipText("Higher is better");
+ } else {
+ covariantLabel = new JLabel("<");
+ covariantLabel.setToolTipText("Lower is better");
+ }
+
+ b.add(covariantLabel, c.xy(1, r));
+
+ // Name label with proper tooltip
+ JLabel nameLabel = new JLabel(check.getName());
+
+ if (check.getDescription() != null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("");
+ sb.append(check.getDescription());
+
+ Constant constant = check.getLowConstant();
+
+ // Tell data unit if relevant
+ sb.append(" ");
+
+ if (constant.getQuantityUnit() != null) {
+ sb.append("Unit=").append(constant.getQuantityUnit());
+ } else {
+ // Otherwise, simply tell the data type
+ sb.append("Type=").append(constant.getShortTypeName());
+ }
+
+ sb.append("");
+
+ nameLabel.setToolTipText(sb.toString());
+ }
+
+ b.add(nameLabel, c.xy(3, r));
+
+ // Value & bound fields
+ b.add(values[ic][0], c.xy(5, r));
+ b.add(bounds[ic][0], c.xy(7, r));
+ b.add(values[ic][1], c.xy(9, r));
+ b.add(bounds[ic][1], c.xy(11, r));
+ b.add(values[ic][2], c.xy(13, r));
+ }
+
+ // Last row for global result
+ r += 2;
+
+ JLabel globalLabel = new JLabel("Result");
+ globalLabel.setToolTipText("Global check result");
+ b.add(globalLabel, c.xy(5, r));
+ b.add(globalField, c.xy(9, r));
+ }
+
+ //-------------------//
+ // createBoundFields //
+ //-------------------//
+ private void createBoundFields ()
+ {
+ // Allocate bound fields (2 per check)
+ final int checkNb = suite.getChecks().size();
+ bounds = new JTextField[checkNb][];
+
+ for (int ic = 0; ic < checkNb; ic++) {
+ Check check = suite.getChecks().get(ic);
+ bounds[ic] = new JTextField[2];
+
+ for (int i = 0; i <= 1; i++) {
+ JTextField field = new JTextField(FIELD_WIDTH);
+ field.setHorizontalAlignment(JTextField.CENTER);
+ bounds[ic][i] = field;
+
+ Constant.Double constant = (i == 0) ? check.getLowConstant()
+ : check.getHighConstant();
+
+ field.setText(textOf(constant.getValue()));
+ field.setToolTipText(
+ "" + constant.getName() + " "
+ + constant.getDescription() + "");
+ }
+ }
+ }
+
+ //--------------//
+ // createLayout //
+ //--------------//
+ private FormLayout createLayout (int checkNb)
+ {
+ // Build proper column specification
+ StringBuilder sbc = new StringBuilder();
+ sbc.append("center:pref").append(", ").append(COLUMN_GAP).append(", "); // Covariance
+ sbc.append(" right:40dlu").append(", ").append(COLUMN_GAP).append(", "); // Name
+ sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", ");
+ sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", "); // Low limit
+ sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", ");
+ sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", "); // High Limit
+ sbc.append(" right:pref");
+
+ // Build proper row specification
+ StringBuilder sbr = new StringBuilder();
+
+ for (int n = 0; n <= checkNb; n++) {
+ if (n != 0) {
+ sbr.append(", ").append(LINE_GAP).append(", ");
+ }
+
+ sbr.append("pref");
+ }
+
+ logger.debug("sb cols={}", sbc);
+ logger.debug("sb rows={}", sbr);
+
+ // Create proper form layout
+ return new FormLayout(
+ sbc.toString(), //cols
+ sbr.toString()); //rows
+ }
+
+ //-------------------//
+ // createValueFields //
+ //-------------------//
+ private void createValueFields ()
+ {
+ // Allocate value fields (3 per check)
+ final int checkNb = suite.getChecks().size();
+ values = new JTextField[checkNb][3];
+
+ for (int n = 0; n < checkNb; n++) {
+ for (int i = 0; i <= 2; i++) {
+ JTextField field = new JTextField(FIELD_WIDTH);
+ field.setEditable(false);
+ field.setHorizontalAlignment(JTextField.CENTER);
+ values[n][i] = field;
+ }
+ }
+ }
+
+ //-------------//
+ // resetValues //
+ //-------------//
+ private void resetValues ()
+ {
+ for (JTextField[] seq : values) {
+ for (JTextField field : seq) {
+ field.setText("");
+ }
+ }
+ }
+
+ //--------//
+ // textOf //
+ //--------//
+ private String textOf (double val)
+ {
+ return String.format(Locale.getDefault(), "%5.2f", val);
+ }
+
+ //---------//
+ // valueOf //
+ //---------//
+ private double valueOf (String text)
+ {
+ Scanner scanner = new Scanner(text);
+ scanner.useLocale(Locale.getDefault());
+
+ while (scanner.hasNext()) {
+ if (scanner.hasNextDouble()) {
+ return scanner.nextDouble();
+ } else {
+ scanner.next();
+ }
+ }
+
+ // Kludge!
+ return Double.NaN;
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //
+ //-------------//
+ // ParamAction //
+ //-------------//
+ private class ParamAction
+ extends AbstractAction
+ {
+ //~ Methods ------------------------------------------------------------
+
+ /**
+ * Method run whenever user presses Return/Enter in one of
+ * the parameter fields
+ */
+ @Override
+ public void actionPerformed (ActionEvent e)
+ {
+ // Any & several bounds may have been modified by the user
+ // Since the same constant can be used in several fields, we have to
+ // take a snapshot of all constants values, before modifying any one
+ Map values = new HashMap<>();
+
+ for (Check check : suite.getChecks()) {
+ values.put(
+ check.getLowConstant(),
+ check.getLowConstant().getValue());
+ values.put(
+ check.getHighConstant(),
+ check.getHighConstant().getValue());
+ }
+
+ boolean modified = false;
+ int ic = -1;
+
+ for (Check check : suite.getChecks()) {
+ ic++;
+
+ // Check the bounds wrt the corresponding fields
+ for (int i = 0; i < 2; i++) {
+ final Constant.Double constant = (i == 0)
+ ? check.getLowConstant()
+ : check.getHighConstant();
+
+ // Simplistic test to detect modification
+ final JTextField field = bounds[ic][i];
+ final String oldString = textOf(values.get(constant)).trim();
+ final String newString = field.getText().trim();
+
+ if (!oldString.equals(newString)) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Check '").append(check.getName()).
+ append("':");
+
+ if (i == 0) {
+ sb.append(" Low");
+ } else {
+ sb.append(" High");
+ }
+
+ sb.append(" bound '").append(constant.getName()).append(
+ "'");
+
+ final String context = sb.toString();
+
+ // Actually convert the value and update the constant
+ try {
+ constant.setValue(valueOf(newString));
+ modified = true;
+ sb.append(" modified from ").append(oldString).
+ append(" to ").append(newString);
+ logger.info(sb.toString());
+ } catch (Exception ex) {
+ logger.warn(
+ "Error in {}, {}",
+ context, ex.getLocalizedMessage());
+ }
+ }
+ }
+ }
+
+ // If at least one modification has been made, update the whole
+ // table with both suite parameters and object results
+ if (modified) {
+ setSuite(suite);
+ passForm(object);
+ }
+ }
+ }
+}
diff --git a/src/main/omr/check/CheckResult.java b/src/main/omr/check/CheckResult.java
new file mode 100644
index 0000000..cd25c3f
--- /dev/null
+++ b/src/main/omr/check/CheckResult.java
@@ -0,0 +1,30 @@
+//----------------------------------------------------------------------------//
+// //
+// C h e c k R e s u l t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+
+/**
+ * Class {@code CheckResult} encapsulates the result of a check,
+ * composed of a value (double) and a flag which can be RED, YELLOW or GREEN.
+ *
+ * @author Hervé Bitteur
+ */
+public class CheckResult
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /** Numerical result value */
+ public double value;
+
+ /** Flag the result (RED, YELLOW, GREEN) */
+ public int flag;
+}
diff --git a/src/main/omr/check/CheckSuite.java b/src/main/omr/check/CheckSuite.java
new file mode 100644
index 0000000..5ce7c0d
--- /dev/null
+++ b/src/main/omr/check/CheckSuite.java
@@ -0,0 +1,351 @@
+//----------------------------------------------------------------------------//
+// //
+// C h e c k S u i t e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Class {@code CheckSuite} represents a suite of homogeneous checks,
+ * that is checks working on the same type.
+ *
+ * Every check in the suite is assigned a weight, to represent its relative
+ * importance in the suite.
+ *
+ * @param the subtype of Checkable-compatible objects used in the
+ * homogeneous collection of checks in this suite
+ *
+ * @author Hervé Bitteur
+ */
+public class CheckSuite
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(CheckSuite.class);
+
+ //~ Instance fields --------------------------------------------------------
+ /** Name of this suite */
+ protected String name;
+
+ /** Minimum threshold for final grade */
+ protected double threshold;
+
+ /** List of checks in the suite */
+ private final List> checks = new ArrayList<>();
+
+ /** List of related weights in the suite */
+ private final List weights = new ArrayList<>();
+
+ /** Total checks weight */
+ private double totalWeight = 0.0d;
+
+ //~ Constructors -----------------------------------------------------------
+ //------------//
+ // CheckSuite //
+ //------------//
+ /**
+ * Create a suite of checks
+ *
+ * @param name the name for the suite (for debug)
+ * @param threshold the threshold to test results
+ */
+ public CheckSuite (String name,
+ double threshold)
+ {
+ this.name = name;
+ this.threshold = threshold;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----//
+ // add //
+ //-----//
+ /**
+ * Add a check to the suite, with its assigned weight
+ *
+ * @param weight the weight of this check in the suite
+ * @param check the check to add to the suite
+ */
+ public void add (double weight,
+ Check check)
+ {
+ checks.add(check);
+ weights.add(weight);
+ totalWeight += weight;
+ }
+
+ //--------//
+ // addAll //
+ //--------//
+ /**
+ * Add all checks of another suite
+ *
+ * @param suite the suite of checks to be appended
+ * @return the suite with checks appended, for easy chaining
+ */
+ public CheckSuite addAll (CheckSuite suite)
+ {
+ int index = 0;
+
+ for (Check check : suite.checks) {
+ double weight = suite.weights.get(index++);
+ add(weight, check);
+ }
+
+ // Allow chaining
+ return this;
+ }
+
+ //------//
+ // dump //
+ //------//
+ /**
+ * Dump a readable description of all checks of this suite.
+ */
+ public void dump ()
+ {
+ StringBuilder sb = new StringBuilder(String.format("%n"));
+
+ if (name != null) {
+ sb.append(name);
+ }
+
+ sb.append(String.format(" Check Suite: threshold=%f%n", threshold));
+
+ dumpSpecific(sb);
+
+ sb.append(String.format(
+ "Weight Name Covariant Low High%n"));
+ sb.append(String.format(
+ "------ ---- ------ --- ----%n"));
+
+ int index = 0;
+
+ for (Check check : checks) {
+ sb.append(String.format(
+ "%4.1f %-19s %5b % 6.2f % 6.2f %n",
+ weights.get(index++),
+ check.getName(),
+ check.isCovariant(),
+ check.getLow(),
+ check.getHigh()));
+ }
+
+ logger.info(sb.toString());
+ }
+
+ //-----------//
+ // getChecks //
+ //-----------//
+ /**
+ * Report the collection of checks that compose this suite.
+ *
+ * @return the collection of checks
+ */
+ public List> getChecks ()
+ {
+ return checks;
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report the name of this suite.
+ *
+ * @return suite name
+ */
+ public String getName ()
+ {
+ return name;
+ }
+
+ //--------------//
+ // getThreshold //
+ //--------------//
+ /**
+ * Report the assigned threshold.
+ *
+ * @return the assigned minimum result
+ */
+ public double getThreshold ()
+ {
+ return threshold;
+ }
+
+ //----------------//
+ // getTotalWeight //
+ //----------------//
+ /**
+ * Report the sum of all individual checks.
+ *
+ * @return the total weight of the checks in the suite
+ */
+ public double getTotalWeight ()
+ {
+ return totalWeight;
+ }
+
+ //------------//
+ // getWeights //
+ //------------//
+ /**
+ * Report the weights of the checks.
+ * (collection parallel to the suite checks)
+ *
+ * @return the collection of checks weights
+ */
+ public List getWeights ()
+ {
+ return weights;
+ }
+
+ //------//
+ // pass //
+ //------//
+ /**
+ * Pass sequentially the checks in the suite, stopping at the first
+ * test with red result.
+ *
+ * @param object the object to be checked
+ * @return the computed grade.
+ */
+ public double pass (C object)
+ {
+ boolean debug = logger.isDebugEnabled() || object.isVip();
+ double grade = 0.0d;
+ CheckResult result = new CheckResult();
+ StringBuilder sb = null;
+
+ if (debug) {
+ sb = new StringBuilder(512);
+ sb.append(name).append(" ").append(object).append(" ");
+ }
+
+ int index = 0;
+
+ for (Check check : checks) {
+ check.pass(object, result, true);
+
+ if (debug) {
+ sb.append(
+ String.format("%15s:%5.2f", check.getName(),
+ result.value));
+ }
+
+ if (result.flag == Check.RED) {
+ // The check totally failed, we give up immediately!
+ if (debug) {
+ logger.info(sb.toString());
+ }
+
+ return result.flag;
+ } else {
+ // Aggregate results
+ double weight = weights.get(index);
+ grade += (result.flag * weight);
+ }
+
+ index++;
+ }
+
+ // Final grade
+ grade /= totalWeight;
+
+ if (debug) {
+ sb.append(String.format(" => %5.2f ", grade));
+ logger.info(sb.toString());
+ }
+
+ return grade;
+ }
+
+ //----------------//
+ // passCollection //
+ //----------------//
+ /**
+ * Pass the whole collection of suites in a row and return
+ * the global result.
+ *
+ * @param The specific type of checked object
+ * @param object the object to be checked
+ * @param suites the collection of check suites to pass
+ *
+ * @return the global result
+ */
+ public static double passCollection (T object,
+ Collection> suites)
+ {
+ double totalWeight = 0.0d;
+ double grade = 0.0d;
+
+ for (CheckSuite suite : suites) {
+ double res = suite.pass(object);
+
+ // If one totally failed, give up immediately
+ if (res == Check.RED) {
+ return res;
+ } else {
+ // Aggregate results
+ double weight = suite.getTotalWeight();
+ totalWeight += weight;
+ grade += (res * weight);
+ }
+ }
+
+ // Final grade
+ return grade / totalWeight;
+ }
+
+ //---------//
+ // setName //
+ //---------//
+ /**
+ * Assings a new name to the check suite.
+ *
+ * @param name the new name
+ */
+ public void setName (String name)
+ {
+ this.name = name;
+ }
+
+ //--------------//
+ // setThreshold //
+ //--------------//
+ /**
+ * Allows to assign a new threshold for the suite.
+ *
+ * @param threshold the new minimum result
+ */
+ public void setThreshold (double threshold)
+ {
+ this.threshold = threshold;
+ }
+
+ //--------------//
+ // dumpSpecific //
+ //--------------//
+ /**
+ * Just an empty placeholder, meant to be overridden.
+ *
+ * @param sb StringBuilder to populate
+ */
+ protected void dumpSpecific (StringBuilder sb)
+ {
+ }
+}
diff --git a/src/main/omr/check/Checkable.java b/src/main/omr/check/Checkable.java
new file mode 100644
index 0000000..7bcf8de
--- /dev/null
+++ b/src/main/omr/check/Checkable.java
@@ -0,0 +1,33 @@
+//----------------------------------------------------------------------------//
+// //
+// C h e c k a b l e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+import omr.util.Vip;
+
+/**
+ * Interface {@code Checkable} describes a class that may be checked and
+ * then assigned a result for that check.
+ *
+ * @author Hervé Bitteur
+ */
+public interface Checkable
+ extends Vip
+{
+ //~ Methods ----------------------------------------------------------------
+
+ /**
+ * Store the check result directly into the checkable entity.
+ *
+ * @param result the result to be stored
+ */
+ void setResult (Result result);
+}
diff --git a/src/main/omr/check/FailureResult.java b/src/main/omr/check/FailureResult.java
new file mode 100644
index 0000000..5c33d6c
--- /dev/null
+++ b/src/main/omr/check/FailureResult.java
@@ -0,0 +1,54 @@
+//----------------------------------------------------------------------------//
+// //
+// F a i l u r e R e s u l t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+
+/**
+ * Class {@code FailureResult} is the root of all results that store a
+ * failure.
+ *
+ * @author Hervé Bitteur
+ */
+public class FailureResult
+ extends Result
+{
+ //~ Constructors -----------------------------------------------------------
+
+ //---------------//
+ // FailureResult //
+ //---------------//
+ /**
+ * Create a new FailureResult object.
+ *
+ * @param comment A comment that describe the failure reason
+ */
+ public FailureResult (String comment)
+ {
+ super(comment);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Report a readable string about this failure instance
+ *
+ * @return a descriptive string
+ */
+ @Override
+ public String toString ()
+ {
+ return "Failure:" + super.toString();
+ }
+}
diff --git a/src/main/omr/check/Result.java b/src/main/omr/check/Result.java
new file mode 100644
index 0000000..f9a202f
--- /dev/null
+++ b/src/main/omr/check/Result.java
@@ -0,0 +1,60 @@
+//----------------------------------------------------------------------------//
+// //
+// R e s u l t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+
+/**
+ * Class {@code Result} is the root of all result information stored while
+ * processing processing checks.
+ *
+ * @author Hervé Bitteur
+ */
+public abstract class Result
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /**
+ * A readable comment about the result.
+ */
+ public final String comment;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //--------//
+ // Result //
+ //--------//
+ /**
+ * Creates a new Result object.
+ *
+ * @param comment A description of this result
+ */
+ public Result (String comment)
+ {
+ this.comment = comment;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Report a description of this result
+ *
+ * @return A descriptive string
+ */
+ @Override
+ public String toString ()
+ {
+ return comment;
+ }
+}
diff --git a/src/main/omr/check/SuccessResult.java b/src/main/omr/check/SuccessResult.java
new file mode 100644
index 0000000..7716ccd
--- /dev/null
+++ b/src/main/omr/check/SuccessResult.java
@@ -0,0 +1,54 @@
+//----------------------------------------------------------------------------//
+// //
+// S u c c e s s R e s u l t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.check;
+
+
+/**
+ * Class {@code SuccessResult} is the root of all results that store a
+ * success.
+ *
+ * @author Hervé Bitteur
+ */
+public class SuccessResult
+ extends Result
+{
+ //~ Constructors -----------------------------------------------------------
+
+ //---------------//
+ // SuccessResult //
+ //---------------//
+ /**
+ * Creates a new SuccessResult object.
+ *
+ * @param comment A description of the success
+ */
+ public SuccessResult (String comment)
+ {
+ super(comment);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Report a description of this success
+ *
+ * @return A descriptive string
+ */
+ @Override
+ public String toString ()
+ {
+ return "Success:" + super.toString();
+ }
+}
diff --git a/src/main/omr/check/package.html b/src/main/omr/check/package.html
new file mode 100644
index 0000000..77612d1
--- /dev/null
+++ b/src/main/omr/check/package.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Package omr.check
+
+
+
+
+ This package defines checks, organized in check suites, to run
+ series of checks to filter out some candidates when looking for
+ precise entities.
+ This filtering technique is quite rough, and
+ should perhaps be replaced by proper use of specific neural
+ networks.
+
+
+
+
diff --git a/src/main/omr/constant/Constant.java b/src/main/omr/constant/Constant.java
new file mode 100644
index 0000000..3e787e3
--- /dev/null
+++ b/src/main/omr/constant/Constant.java
@@ -0,0 +1,923 @@
+//----------------------------------------------------------------------------//
+// //
+// C o n s t a n t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import omr.util.DoubleValue;
+
+import net.jcip.annotations.ThreadSafe;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * This abstract class handles the mapping between one application
+ * variable and a property name and value.
+ * It is meant essentially to handle any kind of symbolic constant, whose value
+ * may have to be tuned and saved for future runs of the application.
+ *
+ * Please refer to {@link ConstantManager} for a detailed explanation on how
+ * the current value of any given Constant is determined at run-time.
+ *
+ *
The class {@code Constant} is not meant to be used directly (it is
+ * abstract), but rather through any of its subclasses:
+ *
+ *
{@link Constant.Angle} {@link Constant.Boolean}
+ * {@link Constant.Color} {@link Constant.Double}
+ * {@link Constant.Integer} {@link Constant.Ratio}
+ * {@link Constant.String}
+ * and others...
+ *
+ * @author Hervé Bitteur
+ */
+@ThreadSafe
+public abstract class Constant
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Constant.class);
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ // Data assigned at construction time
+ //-----------------------------------
+ /** Unit (if relevant) used by the quantity measured. */
+ private final java.lang.String quantityUnit;
+
+ /** Source-provided value to be used if needed. */
+ private final java.lang.String sourceString;
+
+ /** Semantic. */
+ private final java.lang.String description;
+
+ // Data assigned at ConstantSet initMap time
+ //------------------------------------------
+ /** Name of the Constant. */
+ private volatile java.lang.String name;
+
+ /** Fully qualified Constant name. (unit.name) */
+ private volatile java.lang.String qualifiedName;
+
+ // Data modified at any time
+ //--------------------------
+ /** Initial Value (used for reset). Assigned once */
+ private java.lang.String initialString;
+
+ /** Current data. */
+ private AtomicReference tuple = new AtomicReference<>();
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //----------//
+ // Constant //
+ //----------//
+ /**
+ * Creates a constant instance, while providing a default value,
+ * in case the external property is not yet defined.
+ *
+ * @param quantityUnit Unit used as base for measure, if relevant
+ * @param sourceString Source value, expressed by a string literal which
+ * cannot be null
+ * @param description A quick description of the purpose of this constant
+ */
+ protected Constant (java.lang.String quantityUnit,
+ java.lang.String sourceString,
+ java.lang.String description)
+ {
+ if (sourceString == null) {
+ logger.warn(
+ "*** Constant with no sourceString. Description: {}",
+ description);
+ throw new IllegalArgumentException(
+ "Any constant must have a source-provided String");
+ }
+
+ this.quantityUnit = quantityUnit;
+ this.sourceString = sourceString;
+ this.description = description;
+
+ // System.out.println(
+ // Thread.currentThread().getName() + ": " + "-- Creating Constant: " +
+ // description);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //----------//
+ // setValue //
+ //----------//
+ /**
+ * Modify the current value of the constant.
+ * This abstract method is actually defined in each subclass, to enforce
+ * validation of the provided string with respect to the target constant type.
+ *
+ * @param string the new value, as a string to be checked
+ */
+ public abstract void setValue (java.lang.String string);
+
+ //------------------//
+ // getCurrentString //
+ //------------------//
+ /**
+ * Get the current value, as a String type.
+ *
+ * @return the String view of the value
+ */
+ public java.lang.String getCurrentString ()
+ {
+ return getTuple().currentString;
+ }
+
+ //----------------//
+ // getDescription //
+ //----------------//
+ /**
+ * Get the description sentence recorded with the constant
+ *
+ * @return the description sentence as a string
+ */
+ public java.lang.String getDescription ()
+ {
+ return description;
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report the name of the constant
+ *
+ * @return the constant name
+ */
+ public java.lang.String getName ()
+ {
+ return name;
+ }
+
+ //------------------//
+ // getQualifiedName //
+ //------------------//
+ /**
+ * Report the qualified name of the constant
+ *
+ * @return the constant qualified name
+ */
+ public java.lang.String getQualifiedName ()
+ {
+ return qualifiedName;
+ }
+
+ //-----------------//
+ // getQuantityUnit //
+ //-----------------//
+ /**
+ * Report the unit, if any, used as base of quantity measure
+ *
+ * @return the quantity unit, if any
+ */
+ public java.lang.String getQuantityUnit ()
+ {
+ return quantityUnit;
+ }
+
+ //------------------//
+ // getShortTypeName //
+ //------------------//
+ /**
+ * Report the very last part of the type name of the constant
+ *
+ * @return the type name (last part)
+ */
+ public java.lang.String getShortTypeName ()
+ {
+ final java.lang.String typeName = getClass().getName();
+ final int separator = Math.max(
+ typeName.lastIndexOf('$'),
+ typeName.lastIndexOf('.'));
+
+ if (separator != -1) {
+ return typeName.substring(separator + 1);
+ } else {
+ return typeName;
+ }
+ }
+
+ //-----------------//
+ // getSourceString //
+ //-----------------//
+ /**
+ * Report the constant source string
+ *
+ * @return the source string
+ */
+ public java.lang.String getSourceString ()
+ {
+ return sourceString;
+ }
+
+ //------------//
+ // isModified //
+ //------------//
+ /**
+ * Checks whether the current value is different from the original one.
+ * NOTA_BENE: The test is made on string literal, which may result in false
+ * modification signals, simply because the string for example contains an
+ * additional space
+ *
+ * @return The modification status
+ */
+ public boolean isModified ()
+ {
+ return !getCurrentString().equals(initialString);
+ }
+
+ //---------------//
+ // isSourceValue //
+ //---------------//
+ /**
+ * Report whether the current constant value is the source one.
+ * (not altered by either properties read from disk, of value changed later)
+ *
+ * @return true if still the source value, false otherwise
+ */
+ public boolean isSourceValue ()
+ {
+ return getCurrentString().equals(sourceString);
+ }
+
+ //--------//
+ // remove //
+ //--------//
+ /**
+ * Remove a given constant from memory
+ */
+ public void remove ()
+ {
+ ConstantManager.getInstance().removeConstant(this);
+ }
+
+ //-------//
+ // reset //
+ //-------//
+ /**
+ * Forget any modification made, and reset to the source value.
+ */
+ public void reset ()
+ {
+ setTuple(sourceString, decode(sourceString));
+ }
+
+ //---------//
+ // setUnit //
+ //---------//
+ /**
+ * Allows to record the unit and name of the constant.
+ *
+ * @param unit the unit (class name) this constant belongs to
+ * @param name the constant name
+ */
+ public void setUnitAndName (java.lang.String unit,
+ java.lang.String name)
+ {
+ // System.out.println(
+ // Thread.currentThread().getName() + ": " + "Assigning unit:" + unit +
+ // " name:" + name);
+ this.name = name;
+
+ final java.lang.String qName = (unit != null) ? (unit + "." + name) : name;
+
+ // We can now try to register that constant
+ try {
+ java.lang.String prop = ConstantManager.getInstance().addConstant(
+ qName, this);
+
+ // Now we can assign a first current value
+ if (prop != null) {
+ // Use property value
+ setTuple(prop, decode(prop));
+ } else {
+ // Use source value
+ ///logger.info("setUnitAndName. unit:" + unit + " name:" + name);
+ setTuple(sourceString, decode(sourceString));
+ }
+
+ // Very last thing
+ qualifiedName = qName;
+
+ // System.out.println(
+ // Thread.currentThread().getName() + ": " + "Done unit:" + unit +
+ // " name:" + name);
+ } catch (Exception ex) {
+ logger.warn("Error registering constant {}", qName);
+ ex.printStackTrace();
+ }
+ }
+
+ //------------------//
+ // toDetailedString //
+ //------------------//
+ /**
+ * Report detailed data about this constant
+ *
+ * @return data meant for end user
+ */
+ public java.lang.String toDetailedString ()
+ {
+ StringBuilder sb = new StringBuilder(getQualifiedName());
+ sb.append(" (").append(getCurrentString()).append(")");
+ sb.append(" \"").append(getDescription()).append("\"");
+
+ return sb.toString();
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Used by UnitTreeTable to display the name of the constant,
+ * so only the unqualified name is returned.
+ *
+ * @return the (unqualified) constant name
+ */
+ @Override
+ public java.lang.String toString ()
+ {
+ return (name != null) ? name : "*no name*";
+ }
+
+ //--------//
+ // decode //
+ //--------//
+ /**
+ * Convert a given string to the proper object value, as
+ * implemented by each subclass.
+ *
+ * @param str the encoded string
+ * @return the decoded object
+ */
+ protected abstract Object decode (java.lang.String str);
+
+ //----------------//
+ // getCachedValue //
+ //----------------//
+ /**
+ * Report the current value of the constant
+ *
+ * @return the (cached) current value
+ */
+ protected Object getCachedValue ()
+ {
+ return getTuple().cachedValue;
+ }
+
+ //----------//
+ // setTuple //
+ //----------//
+ /**
+ * Modify the current parameter data in an atomic way,
+ * and remember the very first value (the initial string).
+ *
+ * @param str The new value (as a string)
+ * @param val The new value (as an object)
+ */
+ protected void setTuple (java.lang.String str,
+ Object val)
+ {
+ while (true) {
+ Tuple old = tuple.get();
+ Tuple temp = new Tuple(str, val);
+
+ if (old == null) {
+ if (tuple.compareAndSet(null, temp)) {
+ initialString = str;
+
+ return;
+ }
+ } else {
+ tuple.set(temp);
+
+ return;
+ }
+ }
+ }
+
+ //----------------//
+ // getValueOrigin //
+ //----------------//
+ /**
+ * Convenient method, reporting the origin of the current value for
+ * this constant, either SRC or USR.
+ *
+ * @return a mnemonic for the value origin
+ */
+ java.lang.String getValueOrigin ()
+ {
+ ConstantManager mgr = ConstantManager.getInstance();
+ java.lang.String cur = getCurrentString();
+ java.lang.String usr = mgr.getConstantUserValue(qualifiedName);
+ java.lang.String src = sourceString;
+
+ if (cur.equals(src)) {
+ return "SRC";
+ }
+
+ if (cur.equals(usr)) {
+ return "USR";
+ }
+
+ return "???";
+ }
+
+ //------------------//
+ // checkInitialized //
+ //------------------//
+ /**
+ * Check the unit+name have been assigned to this constant object.
+ * They are mandatory to link the constant to the persistency mechanism.
+ */
+ private void checkInitialized ()
+ {
+ int i = 0;
+
+ // Make sure everything is initialized properly
+ while (qualifiedName == null) {
+ i++;
+ UnitManager.getInstance().checkDirtySets();
+ }
+
+ // For monitoring/debugging only
+ if (i > 1) {
+ System.out.println(
+ "*** " + Thread.currentThread().getName()
+ + " checkInitialized loop:" + i);
+ }
+ }
+
+ //----------//
+ // getTuple //
+ //----------//
+ /**
+ * Report the current tuple data, which may imply to trigger the
+ * assignment of qualified name to the constant, in order to get
+ * property data
+ *
+ * @return the current tuple data
+ */
+ private Tuple getTuple ()
+ {
+ checkInitialized();
+
+ return tuple.get();
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-------//
+ // Angle //
+ //-------//
+ /**
+ * A subclass of Double, meant to store an angle (in radians).
+ */
+ public static class Angle
+ extends Constant.Double
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Specific constructor, where 'unit' and 'name' are assigned later
+ *
+ * @param defaultValue the (double) default value
+ * @param description the semantic of the constant
+ */
+ public Angle (double defaultValue,
+ java.lang.String description)
+ {
+ super("Radians", defaultValue, description);
+ }
+ }
+
+ //---------//
+ // Boolean //
+ //---------//
+ /**
+ * A subclass of Constant, meant to store a boolean value.
+ */
+ public static class Boolean
+ extends Constant
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Specific constructor, where 'unit' and 'name' are assigned later
+ *
+ * @param defaultValue the (boolean) default value
+ * @param description the semantic of the constant
+ */
+ public Boolean (boolean defaultValue,
+ java.lang.String description)
+ {
+ super(null, java.lang.Boolean.toString(defaultValue), description);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ /**
+ * Retrieve the current constant value
+ *
+ * @return the current (boolean) value
+ */
+ public boolean getValue ()
+ {
+ return ((java.lang.Boolean) getCachedValue()).booleanValue();
+ }
+
+ /**
+ * Convenient method to access this boolean value
+ *
+ * @return true if set, false otherwise
+ */
+ public boolean isSet ()
+ {
+ return getValue();
+ }
+
+ /**
+ * Allows to set a new boolean value (passed as a string) to this
+ * constant. The string validity is actually checked.
+ *
+ * @param string the boolean value as a string
+ */
+ @Override
+ public void setValue (java.lang.String string)
+ {
+ setValue(java.lang.Boolean.valueOf(string).booleanValue());
+ }
+
+ /**
+ * Set a new value to the constant
+ *
+ * @param val the new (boolean) value
+ */
+ public void setValue (boolean val)
+ {
+ setTuple(java.lang.Boolean.toString(val), val);
+ }
+
+ @Override
+ protected java.lang.Boolean decode (java.lang.String str)
+ {
+ return java.lang.Boolean.valueOf(str);
+ }
+ }
+
+ //-------//
+ // Color //
+ //-------//
+ /**
+ * A subclass of Constant, meant to store a {@link java.awt.Color}
+ * value.
+ * They have a disk repository which is separate from the other constants.
+ */
+ public static class Color
+ extends Constant
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Normal constructor, with a String type for default value
+ *
+ * @param unit the enclosing unit
+ * @param name the constant name
+ * @param defaultValue the default (String) RGB value
+ * @param description the semantic of the constant
+ */
+ public Color (java.lang.String unit,
+ java.lang.String name,
+ java.lang.String defaultValue,
+ java.lang.String description)
+ {
+ super(null, defaultValue, description);
+ setUnitAndName(unit, name);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ //-------------//
+ // decodeColor //
+ //-------------//
+ public static java.awt.Color decodeColor (java.lang.String str)
+ {
+ return java.awt.Color.decode(str);
+ }
+
+ //-------------//
+ // encodeColor //
+ //-------------//
+ public static java.lang.String encodeColor (java.awt.Color color)
+ {
+ return java.lang.String.format(
+ "#%02x%02x%02x",
+ color.getRed(),
+ color.getGreen(),
+ color.getBlue());
+ }
+
+ /**
+ * Retrieve the current constant value
+ *
+ * @return the current (Color) value
+ */
+ public java.awt.Color getValue ()
+ {
+ return (java.awt.Color) getCachedValue();
+ }
+
+ /**
+ * Allows to set a new int RGB value (passed as a string) to this
+ * constant. The string validity is actually checked.
+ *
+ * @param string the int value as a string
+ */
+ @Override
+ public void setValue (java.lang.String string)
+ {
+ setValue(java.awt.Color.decode(string));
+ }
+
+ /**
+ * Set a new value to the constant
+ *
+ * @param val the new Color value
+ */
+ public void setValue (java.awt.Color val)
+ {
+ setTuple(encodeColor(val), val);
+ }
+
+ @Override
+ protected java.awt.Color decode (java.lang.String str)
+ {
+ return decodeColor(str);
+ }
+ }
+
+ //--------//
+ // Double //
+ //--------//
+ /**
+ * A subclass of Constant, meant to store a double value.
+ */
+ public static class Double
+ extends Constant
+ {
+ //~ Constructors -------------------------------------------------------
+
+ public Double (java.lang.String quantityUnit,
+ double defaultValue,
+ java.lang.String description)
+ {
+ super(
+ quantityUnit,
+ java.lang.Double.toString(defaultValue),
+ description);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ public double getValue ()
+ {
+ return ((DoubleValue) getCachedValue()).doubleValue();
+ }
+
+ public DoubleValue getWrappedValue ()
+ {
+ // Return a copy
+ return new DoubleValue(getValue());
+ }
+
+ public void setValue (double val)
+ {
+ setTuple(java.lang.Double.toString(val), new DoubleValue(val));
+ }
+
+ public void setValue (DoubleValue val)
+ {
+ setTuple(val.toString(), val);
+ }
+
+ @Override
+ public void setValue (java.lang.String string)
+ {
+ setValue(decode(string));
+ }
+
+ @Override
+ protected DoubleValue decode (java.lang.String str)
+ {
+ return new DoubleValue(java.lang.Double.valueOf(str));
+ }
+ }
+
+ //---------//
+ // Integer //
+ //---------//
+ /**
+ * A subclass of Constant, meant to store an int value.
+ */
+ public static class Integer
+ extends Constant
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Specific constructor, where 'unit' and 'name' are assigned later
+ *
+ * @param quantityUnit unit used by this value
+ * @param defaultValue the (int) default value
+ * @param description the semantic of the constant
+ */
+ public Integer (java.lang.String quantityUnit,
+ int defaultValue,
+ java.lang.String description)
+ {
+ super(
+ quantityUnit,
+ java.lang.Integer.toString(defaultValue),
+ description);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ /**
+ * Retrieve the current constant value
+ *
+ * @return the current (int) value
+ */
+ public int getValue ()
+ {
+ return (java.lang.Integer) getCachedValue();
+ }
+
+ /**
+ * Allows to set a new int value (passed as a string) to this
+ * constant. The string validity is actually checked.
+ *
+ * @param string the int value as a string
+ */
+ @Override
+ public void setValue (java.lang.String string)
+ {
+ setValue(java.lang.Integer.valueOf(string).intValue());
+ }
+
+ /**
+ * Set a new value to the constant
+ *
+ * @param val the new (int) value
+ */
+ public void setValue (int val)
+ {
+ setTuple(java.lang.Integer.toString(val), val);
+ }
+
+ @Override
+ protected java.lang.Integer decode (java.lang.String str)
+ {
+ return new java.lang.Integer(str);
+ }
+ }
+
+ //-------//
+ // Ratio //
+ //-------//
+ /**
+ * A subclass of Double, meant to store a ratio or percentage.
+ */
+ public static class Ratio
+ extends Constant.Double
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Specific constructor, where 'unit' and 'name' are assigned later
+ *
+ * @param defaultValue the (double) default value
+ * @param description the semantic of the constant
+ */
+ public Ratio (double defaultValue,
+ java.lang.String description)
+ {
+ super(null, defaultValue, description);
+ }
+ }
+
+ //--------//
+ // String //
+ //--------//
+ /**
+ * A subclass of Constant, meant to store a string value.
+ */
+ public static class String
+ extends Constant
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Normal constructor, with a string type for default value
+ *
+ * @param unit the enclosing unit
+ * @param name the constant name
+ * @param defaultValue the default (string) value
+ * @param description the semantic of the constant
+ */
+ public String (java.lang.String unit,
+ java.lang.String name,
+ java.lang.String defaultValue,
+ java.lang.String description)
+ {
+ this(defaultValue, description);
+ setUnitAndName(unit, name);
+ }
+
+ /**
+ * Specific constructor, where 'unit' and 'name' are assigned later
+ *
+ * @param defaultValue the (string) default value
+ * @param description the semantic of the constant
+ */
+ public String (java.lang.String defaultValue,
+ java.lang.String description)
+ {
+ super(null, defaultValue, description);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ /**
+ * Retrieve the current constant value.
+ * Actually this is synonymous with currentString()
+ *
+ * @return the current (string) value
+ */
+ public java.lang.String getValue ()
+ {
+ return (java.lang.String) getCachedValue();
+ }
+
+ /**
+ * Set a new string value to the constant
+ *
+ * @param val the new (string) value
+ */
+ @Override
+ public void setValue (java.lang.String val)
+ {
+ setTuple(val, val);
+ }
+
+ @Override
+ protected java.lang.String decode (java.lang.String str)
+ {
+ return str;
+ }
+ }
+
+ //-------//
+ // Tuple //
+ //-------//
+ /**
+ * Class used to handle the tuple [currentString + currentValue]
+ * in an atomic way.
+ */
+ private static class Tuple
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ final java.lang.String currentString;
+
+ final Object cachedValue;
+
+ //~ Constructors -------------------------------------------------------
+ public Tuple (java.lang.String currentString,
+ Object cachedValue)
+ {
+ /** Current string Value */
+ this.currentString = currentString;
+
+ /** Current cached Value (optimized) */
+ this.cachedValue = cachedValue;
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public java.lang.String toString ()
+ {
+ return currentString;
+ }
+ }
+}
diff --git a/src/main/omr/constant/ConstantManager.java b/src/main/omr/constant/ConstantManager.java
new file mode 100644
index 0000000..04a7925
--- /dev/null
+++ b/src/main/omr/constant/ConstantManager.java
@@ -0,0 +1,453 @@
+//----------------------------------------------------------------------------//
+// //
+// C o n s t a n t M a n a g e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import omr.Main;
+import omr.WellKnowns;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.jcip.annotations.ThreadSafe;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Class {@code ConstantManager} manages the persistency of the whole
+ * population of Constants, including their mapping to properties,
+ * their storing on disk and their reloading from disk.
+ *
+ *
+ * The actual value of an application "constant", as returned by the method
+ * {@link Constant#getCurrentString}, is determined in the following order, any
+ * definition overriding the previous ones:
+ *
+ * First, SOURCE values are always provided within
+ * source declaration of the constants in the Java source file
+ * itself.
+ * For example, in the "omr/sheet/ScaleBuilder.java" file,
+ * we can find the following declaration which defines the minimum value for
+ * sheet resolution, here specified in pixels (the application has difficulties
+ * with scans of lower resolution).
+ *
+ *
+ * Constant.Integer minResolution = new Constant.Integer(
+ * "Pixels",
+ * 11,
+ * "Minimum resolution, expressed as number of pixels per interline");
+ * This declaration must be read as follows:
+ *
+ * {@code minResolution} is the Java object used in the application.
+ * It is defined as a Constant.Integer, a subtype of Constant meant to host
+ * Integer values
+ *
+ * {@code "Pixels"} specifies the unit used. Here we are counting in
+ * pixels.
+ *
+ * {@code 11} is the constant value. This is the value used by the
+ * application, provided it is not overridden in the USER properties file
+ * or later via a dedicated GUI tool.
+ *
+ * "Minimum resolution, expressed as number of pixels per interline"
+ * is the constant description, which will be used as a tool tip in
+ * the GUI interface in charge of editing these constants.
+ *
+ *
+ *
+ * Then, USER values, contained in a property file named
+ * "run.properties" can assign overriding values to some
+ * constants. For example, the {@code minInterline} constant
+ * above could be altered by the following line in this user file:
+ *
+ * omr.sheet.ScaleBuilder.minInterline=12
+ * This file is modified every time the user updates the value of a constant by
+ * means of the provided Constant user interface at run-time.
+ * The file is not mandatory, and is located in the user application data
+ * {@code config} folder.
+ * Its values override the SOURCE corresponding constants.
+ * Typically, these USER values represent some modification made by the end user
+ * at run-time and thus saved from one run to the other.
+ * The file is not meant to be edited manually, but rather through the provided
+ * GUI tool.
+ *
+ *
+ * Then, CLI values, as set on the command line interface, by means
+ * of the "-option" key=value command. For further details on
+ * this command, refer to the {@link omr.CLI} class documentation.
+ * Persistency here depends on the way Audiveris is running:
+ * When running in batch mode, these CLI-defined constant values
+ * are not persisted in the USER file, unless the constant
+ * {@code omr.Main.persistBatchCliConstants} is set to true.
+ * When running in interactive mode, these CLI-defined constant
+ * values are always persisted in the USER file.
+ *
+ * Finally, UI Options Menu values, as set online through the
+ * graphical user interface. These constant values defined at the GUI level are
+ * persisted in the USER file.
+ *
+ * The whole set of constant values is stored on disk when the application
+ * is closed. Doing so, the disk values are always kept in synch with the
+ * program values, provided the application is normally closed rather than
+ * killed . It can also be stored programmatically by calling the
+ * {@link #storeResource} method.
+ *
+ *
Only the USER property file is written, the SOURCE values in the source
+ * code are not altered. Moreover, if the user has modified a value in such a
+ * way that the final value is the same as in the source, the value is simply
+ * discarded from the USER property file.
+ * Doing so, the USER property file really contains only the additions of this
+ * particular user.
+ *
+ * @author Hervé Bitteur
+ */
+@ThreadSafe
+public class ConstantManager
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ ConstantManager.class);
+
+ /** User properties file name */
+ private static final String USER_FILE_NAME = "run.properties";
+
+ /** The singleton */
+ private static final ConstantManager INSTANCE = new ConstantManager();
+
+ //~ Instance fields --------------------------------------------------------
+ /**
+ * Map of all constants created in the application, regardless whether these
+ * constants are enclosed in a ConstantSet or defined as standalone entities
+ */
+ protected final ConcurrentHashMap constants = new ConcurrentHashMap<>();
+
+ /** User properties */
+ private final UserHolder userHolder = new UserHolder(
+ new File(WellKnowns.CONFIG_FOLDER, USER_FILE_NAME));
+
+ //~ Constructors -----------------------------------------------------------
+ //-----------------//
+ // ConstantManager //
+ //-----------------//
+ private ConstantManager ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //------------------//
+ // getAllProperties //
+ //------------------//
+ /**
+ * Report the whole collection of properties (coming from USER
+ * sources) backed up on disk.
+ *
+ * @return the collection of constant properties
+ */
+ public Collection getAllProperties ()
+ {
+ SortedSet props = new TreeSet<>(userHolder.getKeys());
+
+ return props;
+ }
+
+ //-------------//
+ // getInstance //
+ //-------------//
+ /**
+ * Report the singleton of this class.
+ *
+ * @return the only ConstantManager instance
+ */
+ public static ConstantManager getInstance ()
+ {
+ return INSTANCE;
+ }
+
+ //-------------//
+ // addConstant //
+ //-------------//
+ /**
+ * Register a brand new constant with a provided name to retrieve
+ * a predefined value loaded from disk backup if any.
+ *
+ * @param qName the constant qualified name
+ * @param constant the Constant instance to register
+ * @return the loaded value if any, otherwise null
+ */
+ public String addConstant (String qName,
+ Constant constant)
+ {
+ if (qName == null) {
+ throw new IllegalArgumentException(
+ "Attempt to add a constant with no qualified name");
+ }
+
+ Constant old = constants.putIfAbsent(qName, constant);
+
+ if ((old != null) && (old != constant)) {
+ throw new IllegalArgumentException(
+ "Attempt to duplicate constant " + qName);
+ }
+
+ // Value set at CLI level?
+ Properties cliConstants = Main.getCliConstants();
+
+ if (cliConstants != null) {
+ String cliValue = cliConstants.getProperty(qName);
+
+ if (cliValue != null) {
+ return cliValue;
+ }
+ }
+
+ // Fallback on using user value
+ return userHolder.getProperty(qName);
+ }
+
+ //-------------------------//
+ // getUnusedUserProperties //
+ //-------------------------//
+ /**
+ * Report the collection of USER properties that do not relate to
+ * any known application Constant.
+ *
+ * @return the potential old stuff in USER properties
+ */
+ public Collection getUnusedUserProperties ()
+ {
+ return userHolder.getUnusedKeys();
+ }
+
+ //----------------//
+ // removeConstant //
+ //----------------//
+ /**
+ * Remove a constant.
+ *
+ * @param constant the constant to remove
+ * @return the removed Constant, or null if not found
+ */
+ public Constant removeConstant (Constant constant)
+ {
+ if (constant.getQualifiedName() == null) {
+ throw new IllegalArgumentException(
+ "Attempt to remove a constant with no qualified name defined");
+ }
+
+ return constants.remove(constant.getQualifiedName());
+ }
+
+ //---------------//
+ // storeResource //
+ //---------------//
+ /**
+ * Stores the current content of the whole property set to disk.
+ * More specifically, only the values set OUTSIDE the original Default
+ * parts are stored, and they are stored in the user property file.
+ */
+ public void storeResource ()
+ {
+ userHolder.store();
+ }
+
+ //----------------------//
+ // getConstantUserValue //
+ //----------------------//
+ String getConstantUserValue (String qName)
+ {
+ return userHolder.getProperty(qName);
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //----------------//
+ // AbstractHolder //
+ //----------------//
+ private class AbstractHolder
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** Related file */
+ protected final File file;
+
+ /** The handled properties */
+ protected Properties properties;
+
+ //~ Constructors -------------------------------------------------------
+ public AbstractHolder (File file)
+ {
+ this.file = file;
+ }
+
+ //~ Methods ------------------------------------------------------------
+ public Collection getKeys ()
+ {
+ Collection strings = new ArrayList<>();
+
+ for (Object obj : properties.keySet()) {
+ strings.add((String) obj);
+ }
+
+ return strings;
+ }
+
+ public String getProperty (String key)
+ {
+ return properties.getProperty(key);
+ }
+
+ public Collection getUnusedKeys ()
+ {
+ SortedSet props = new TreeSet<>();
+
+ for (Object obj : properties.keySet()) {
+ if (!constants.containsKey((String) obj)) {
+ props.add((String) obj);
+ }
+ }
+
+ return props;
+ }
+
+ public Collection getUselessKeys ()
+ {
+ SortedSet props = new TreeSet<>();
+
+ for (Entry entry : properties.entrySet()) {
+ Constant constant = constants.get((String) entry.getKey());
+
+ if ((constant != null)
+ && constant.getSourceString().equals(entry.getValue())) {
+ props.add((String) entry.getKey());
+ }
+ }
+
+ return props;
+ }
+
+ public void load ()
+ {
+ // Load from local file
+ if (file != null) {
+ loadFromFile();
+ }
+ }
+
+ private void loadFromFile ()
+ {
+
+
+ try (InputStream in = new FileInputStream(file)) {
+ properties.load(in);
+ } catch (FileNotFoundException ignored) {
+ // This is not at all an error
+ logger.debug("[{}" + "]" + " No property file {}",
+ ConstantManager.class.getName(),
+ file.getAbsolutePath());
+ } catch (IOException ex) {
+ logger.error("Error loading constants file {}",
+ file.getAbsolutePath());
+ }
+ }
+ }
+
+ //------------//
+ // UserHolder //
+ //------------//
+ /**
+ * Triggers the loading of user property file.
+ * Any modification made at run-time will be saved in the user part.
+ */
+ private class UserHolder
+ extends AbstractHolder
+ {
+
+ //~ Constructors -------------------------------------------------------
+ public UserHolder (File file)
+ {
+ super(file);
+ properties = new Properties();
+ load();
+ }
+
+ //~ Methods ------------------------------------------------------------
+ /**
+ * Remove from the USER collection the properties that are
+ * already in the source with identical value,
+ * and insert properties that need to reflect the current values
+ * which differ from source.
+ */
+ public void cleanup ()
+ {
+ // Browse all constant entries
+ for (Entry entry : constants.entrySet()) {
+ final String key = entry.getKey();
+ final Constant constant = entry.getValue();
+
+ final String current = constant.getCurrentString();
+ final String source = constant.getSourceString();
+
+ if (!current.equals(source)) {
+ logger.debug(
+ "Writing User value for key: {} = {}",
+ key, current);
+
+ properties.setProperty(key, current);
+ } else {
+ if (properties.remove(key) != null) {
+ logger.debug(
+ "Removing User value for key: {} = {}",
+ key, current);
+ }
+ }
+ }
+ }
+
+ public void store ()
+ {
+ // First purge properties
+ cleanup();
+
+ // Then, save the remaining values
+ logger.debug("Store constants into {}", file);
+
+ // First make sure the directory exists (Brenton patch)
+ if (file.getParentFile().mkdirs()) {
+ logger.info("Creating {}", file);
+ }
+
+ // Then write down the properties
+ try (FileOutputStream out = new FileOutputStream(file)) {
+ properties.store(out,
+ " Audiveris user properties file. Do not edit");
+ } catch (FileNotFoundException ex) {
+ logger.warn("Property file {} not found or not writable",
+ file.getAbsolutePath());
+ } catch (IOException ex) {
+ logger.warn("Error while storing the property file {}",
+ file.getAbsolutePath());
+ }
+ }
+ }
+}
diff --git a/src/main/omr/constant/ConstantSet.java b/src/main/omr/constant/ConstantSet.java
new file mode 100644
index 0000000..8204532
--- /dev/null
+++ b/src/main/omr/constant/ConstantSet.java
@@ -0,0 +1,289 @@
+//----------------------------------------------------------------------------//
+// //
+// C o n s t a n t S e t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import net.jcip.annotations.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * This abstract class handles a set of Constants as a whole. In particular,
+ * this allows a user interface (such as {@link UnitTreeTable}) to present an
+ * editing table of the whole set of constants.
+ *
+ * We recommend to define only one such static ConstantSet per class/unit as
+ * a subclass of this (abstract) ConstantSet.
+ *
+ * @author Hervé Bitteur
+ */
+@ThreadSafe
+public abstract class ConstantSet
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(ConstantSet.class);
+
+ //~ Instance fields --------------------------------------------------------
+ /** Name of the containing unit/class */
+ private final String unit;
+
+ /**
+ * The mapping between constant name & constant object. We use a sorted map
+ * to allow access by constant index in constant set, as required by
+ * ConstantTreeTable. This instance can only be lazily constructed, thanks
+ * to {@link #getMap} method, since all the enclosed constants must have
+ * been constructed beforehand.
+ */
+ private volatile SortedMap map;
+
+ //~ Constructors -----------------------------------------------------------
+ //-------------//
+ // ConstantSet //
+ //-------------//
+ /**
+ * A new ConstantSet instance is created, and registered at the UnitManager
+ * singleton, but its map of internal constants will need to be built later.
+ */
+ public ConstantSet ()
+ {
+ unit = getClass().getDeclaringClass().getName();
+
+ // System.out.println(
+ // "\n" + Thread.currentThread().getName() +
+ // ": Creating ConstantSet " + unit);
+
+ // Register this instance
+ UnitManager.getInstance().addSet(this);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //--------//
+ // dumpOf //
+ //--------//
+ /**
+ * A utility method to dump current value of each constant in the set.
+ *
+ * @return the string representation of this set
+ */
+ public String dumpOf ()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("[%s]%n", unit));
+
+ for (Constant constant : getMap().values()) {
+ String origin = constant.getValueOrigin();
+ if (origin.equals("SRC")) {
+ origin = "";
+ } else {
+ origin = "[" + origin + "]";
+ }
+ sb.append(String.format(
+ "%-25s %12s %-14s =%5s %-25s\t%s%n",
+ constant.getName(),
+ constant.getShortTypeName(),
+ (constant.getQuantityUnit() != null)
+ ? ("(" + constant.getQuantityUnit() + ")") : "",
+ origin,
+ constant.getCurrentString(),
+ constant.getDescription()));
+ }
+
+ return sb.toString();
+ }
+
+ //-------------//
+ // getConstant //
+ //-------------//
+ /**
+ * Report a constant knowing its name in the constant set
+ *
+ * @param name the desired name
+ *
+ * @return the proper constant, or null if not found
+ */
+ public Constant getConstant (String name)
+ {
+ return getMap().get(name);
+ }
+
+ //-------------//
+ // getConstant //
+ //-------------//
+ /**
+ * Report a constant knowing its index in the constant set
+ *
+ * @param i the desired index value
+ *
+ * @return the proper constant
+ */
+ public Constant getConstant (int i)
+ {
+ return Collections.list(Collections.enumeration(getMap().values())).get(
+ i);
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report the name of the enclosing unit
+ *
+ * @return unit name
+ */
+ public String getName ()
+ {
+ return unit;
+ }
+
+ //------------//
+ // initialize //
+ //------------//
+ /**
+ * Make sure this ConstantSet has properly been initialized (its map of
+ * constants has been built)
+ *
+ * @return true if initialized correctly, false otherwise
+ */
+ public boolean initialize ()
+ {
+ return getMap() != null;
+ }
+
+ //------------//
+ // isModified //
+ //------------//
+ /**
+ * Predicate to check whether at least one of the constant of the set has
+ * been modified
+ *
+ * @return the modification status of the whole set
+ */
+ public boolean isModified ()
+ {
+ for (Constant constant : getMap().values()) {
+ if (constant.isModified()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ //------//
+ // size //
+ //------//
+ /**
+ * Report the number of constants in this constant set
+ *
+ * @return the size of the constant set
+ */
+ public int size ()
+ {
+ return getMap().size();
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Return the last part of the ConstantSet name, without the leading package
+ * names. This short name is used by Constant TreeTable
+ *
+ * @return just the (unqualified) name of the ConstantSet
+ */
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ //sb.append("");
+ int dot = unit.lastIndexOf('.');
+
+ if (dot != -1) {
+ sb.append(unit.substring(dot + 1));
+ } else {
+ sb.append(unit);
+ }
+
+ //sb.append(" ");
+ return sb.toString();
+ }
+
+ //--------//
+ // getMap //
+ //--------//
+ private SortedMap getMap ()
+ {
+ if (map == null) {
+ // Initialize map content
+ initMap();
+ }
+
+ return map;
+ }
+
+ //---------//
+ // initMap //
+ //---------//
+ /**
+ * Now that the enclosed constants of this set have been constructed, let
+ * assign them their unit and name parameters.
+ */
+ private void initMap ()
+ {
+ SortedMap tempMap = new TreeMap<>();
+
+ // Retrieve values of all fields
+ Class> cl = getClass();
+
+ try {
+ for (Field field : cl.getDeclaredFields()) {
+ field.setAccessible(true);
+
+ String name = field.getName();
+
+ // Make sure that we have only Constants in this ConstantSet
+ Object obj = field.get(this);
+
+ // Not yet allocated, no big deal, we'll get back to it later
+ if (obj == null) {
+ ///logger.warn("ConstantSet not fully allocated yet");
+ return;
+ }
+
+ if (obj instanceof Constant) {
+ Constant constant = (Constant) obj;
+ constant.setUnitAndName(unit, name);
+ tempMap.put(name, constant);
+ } else {
+ logger.error(
+ "ConstantSet in unit ''{}'' contains a non"
+ + " Constant field ''{}'' obj= {}",
+ unit, name, obj);
+ }
+ }
+
+ // Assign the constructed map atomically
+ map = tempMap;
+ } catch (SecurityException | IllegalArgumentException |
+ IllegalAccessException ex) {
+ logger.warn("Error initializing map of ConstantSet " + this, ex);
+ }
+ }
+}
diff --git a/src/main/omr/constant/Node.java b/src/main/omr/constant/Node.java
new file mode 100644
index 0000000..3fce692
--- /dev/null
+++ b/src/main/omr/constant/Node.java
@@ -0,0 +1,103 @@
+//----------------------------------------------------------------------------//
+// //
+// N o d e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import java.util.Comparator;
+
+/**
+ * Abstract class {@code Node} represents a node in the hierarchy of
+ * packages and units (aka classes).
+ *
+ * @author Hervé Bitteur
+ */
+public abstract class Node
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** For comparing Node instances according to their name */
+ public static final Comparator nameComparator = new Comparator() {
+ @Override
+ public int compare (Node n1,
+ Node n2)
+ {
+ return n1.getName()
+ .compareTo(n2.getName());
+ }
+ };
+
+
+ //~ Instance fields --------------------------------------------------------
+
+ /** (Fully qualified) name of the node */
+ private final String name;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //------//
+ // Node //
+ //------//
+ /**
+ * Create a new Node.
+ * @param name the fully qualified node name
+ */
+ public Node (String name)
+ {
+ this.name = name;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report the fully qualified name for this node.
+ * @return the fully qualified node name
+ */
+ public String getName ()
+ {
+ return name;
+ }
+
+ //---------------//
+ // getSimpleName //
+ //---------------//
+ /**
+ * Return the last path component (non-qualified).
+ * @return the non-qualified node name
+ */
+ public String getSimpleName ()
+ {
+ int dot = name.lastIndexOf('.');
+
+ if (dot != -1) {
+ return name.substring(dot + 1);
+ } else {
+ return name;
+ }
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Since {@code toString()} is used by JTreeTable to display the
+ * node name, this method returns the last path component of the
+ * node, in other words the non-qualified name.
+ * @return the non-qualified node name
+ */
+ @Override
+ public String toString ()
+ {
+ return getSimpleName();
+ }
+}
diff --git a/src/main/omr/constant/PackageNode.java b/src/main/omr/constant/PackageNode.java
new file mode 100644
index 0000000..897ba76
--- /dev/null
+++ b/src/main/omr/constant/PackageNode.java
@@ -0,0 +1,100 @@
+//----------------------------------------------------------------------------//
+// //
+// P a c k a g e N o d e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import java.util.concurrent.ConcurrentSkipListSet;
+
+/**
+ * Class {@code PackageNode} represents a package in the hierarchy of
+ * nodes. It can have children, which can be sub-packages and units. For
+ * example, the unit/class omr.score.Page will need PackageNode
+ * omr and PackageNode omr.score .
+ *
+ * @author Hervé Bitteur
+ */
+public class PackageNode
+ extends Node
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /**
+ * The children, composed of either other {@code PackageNode} or
+ * {@code ConstantSet}.
+ */
+ private final ConcurrentSkipListSet children = new ConcurrentSkipListSet<>(
+ Node.nameComparator);
+
+ //~ Constructors -----------------------------------------------------------
+
+ //-------------//
+ // PackageNode //
+ //-------------//
+ /**
+ * Create a new PackageNode.
+ *
+ * @param name the fully qualified package name
+ * @param child the first child of the package (either a sub-package, or a
+ * ConstantSet).
+ */
+ public PackageNode (String name,
+ Node child)
+ {
+ super(name);
+
+ if (child != null) {
+ addChild(child);
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------//
+ // addChild //
+ //----------//
+ /**
+ * Add a child to the package children
+ *
+ * @param obj the child to add (sub-package or ConstantSet)
+ */
+ public final void addChild (Node obj)
+ {
+ children.add(obj);
+ }
+
+ //----------//
+ // getChild //
+ //----------//
+ /**
+ * Return the child at given index
+ *
+ * @param index the position in the ordered children list
+ *
+ * @return the desired child
+ */
+ public Object getChild (int index)
+ {
+ return children.toArray()[index];
+ }
+
+ //---------------//
+ // getChildCount //
+ //---------------//
+ /**
+ * Return the number of children currently in this package node
+ *
+ * @return the count of children
+ */
+ public int getChildCount ()
+ {
+ return children.size();
+ }
+}
diff --git a/src/main/omr/constant/UnitManager.java b/src/main/omr/constant/UnitManager.java
new file mode 100644
index 0000000..5d480bd
--- /dev/null
+++ b/src/main/omr/constant/UnitManager.java
@@ -0,0 +1,525 @@
+//----------------------------------------------------------------------------//
+// //
+// U n i t M a n a g e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.jcip.annotations.ThreadSafe;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+/**
+ * Class {@code UnitManager} manages all units (aka classes),
+ * for which we have a ConstantSet.
+ *
+ * To help {@link UnitTreeTable} display the whole tree of UnitNodes,
+ * UnitManager can pre-load all the classes known to contain a ConstantSet.
+ * This list is kept up-to-date and stored as a property.
+ *
+ * @author Hervé Bitteur
+ */
+@ThreadSafe
+public class UnitManager
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Name of this unit */
+ private static final String UNIT = UnitManager.class.getName();
+
+ /** The single instance of this class */
+ private static final UnitManager INSTANCE = new UnitManager();
+
+ /** Separator used in property that concatenates all unit names */
+ private static final String SEPARATOR = ";";
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(UnitManager.class);
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** The root node. */
+ private final PackageNode root = new PackageNode("", null);
+
+ /** Map of PackageNodes and UnitNodes. */
+ private final ConcurrentHashMap mapOfNodes = new ConcurrentHashMap<>();
+
+ /** Set of names of ConstantSets that still need to be initialized. */
+ private final ConcurrentSkipListSet dirtySets = new ConcurrentSkipListSet<>();
+
+ /**
+ * Lists of all units known as containing a constantset.
+ * This is kept up-to-date and saved as a property.
+ */
+ private Constant.String units;
+
+ /** Flag to avoid storing units being pre-loaded. */
+ private volatile boolean storeIt = false;
+
+ //~ Constructors -----------------------------------------------------------
+ //-------------//
+ // UnitManager //
+ //-------------//
+ /** This is a singleton. */
+ private UnitManager ()
+ {
+ mapOfNodes.put("", root);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-------------//
+ // getInstance //
+ //-------------//
+ /**
+ * Report the single instance of this package.
+ *
+ * @return the single instance
+ */
+ public static UnitManager getInstance ()
+ {
+ return INSTANCE;
+ }
+
+ //--------//
+ // addSet //
+ //--------//
+ /**
+ * Add a ConstantSet, which means perhaps adding a UnitNode if not
+ * already allocated and setting its ConstantSet reference to the
+ * provided ConstantSet.
+ *
+ * @param set the ConstantSet to add to the hierarchy
+ */
+ public void addSet (ConstantSet set)
+ {
+ ///log ("addSet set=" + set.getName());
+ retrieveUnit(set.getName()).setConstantSet(set);
+
+ // Register this name in the dirty ones
+ dirtySets.add(set.getName());
+ }
+
+ //---------------//
+ // checkAllUnits //
+ //---------------//
+ /**
+ * Check if all defined constants are used by at least one unit.
+ */
+ public void checkAllUnits ()
+ {
+ SortedSet constants = new TreeSet<>();
+
+ for (Node node : mapOfNodes.values()) {
+ if (node instanceof UnitNode) {
+ UnitNode unit = (UnitNode) node;
+ ConstantSet set = unit.getConstantSet();
+
+ if (set != null) {
+ for (int i = 0; i < set.size(); i++) {
+ Constant constant = set.getConstant(i);
+ constants.add(
+ unit.getName() + "." + constant.getName());
+ }
+ }
+ }
+ }
+
+ dumpStrings("constants", constants);
+
+ Collection props = ConstantManager.getInstance().
+ getAllProperties();
+ props.removeAll(constants);
+ dumpStrings("Non set-enclosed properties", props);
+
+ dumpStrings(
+ "Unused User properties",
+ ConstantManager.getInstance().getUnusedUserProperties());
+ }
+
+ //----------------//
+ // checkDirtySets //
+ //----------------//
+ /**
+ * Go through all registered to-be-initialized sets, and initialize
+ * them, then clear the set of such dirty sets.
+ */
+ public void checkDirtySets ()
+ {
+ int rookies = 0;
+
+ // We use (and clear) the collection of rookies
+ for (Iterator it = dirtySets.iterator(); it.hasNext();) {
+ String name = it.next();
+ rookies++;
+
+ // System.out.println(
+ // Thread.currentThread().getName() + ": checkDirtySets. name=" +
+ // name);
+ UnitNode unit = (UnitNode) getNode(name);
+
+ if (unit.getConstantSet().initialize()) {
+ it.remove();
+ }
+ }
+
+ // System.out.println(
+ // Thread.currentThread().getName() + ": checkDirtySets. rookies:" +
+ // rookies);
+ }
+
+ //--------------//
+ // dumpAllUnits //
+ //--------------//
+ /**
+ * Dumps on the standard output the current value of all Constants
+ * of all ConstantSets.
+ */
+ public void dumpAllUnits ()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("UnitManager. All Units:%n"));
+
+ // Use alphabetical order for easier reading
+ List nodes = new ArrayList<>(mapOfNodes.values());
+ Collections.sort(nodes, Node.nameComparator);
+
+ for (Node node : nodes) {
+ if (node instanceof UnitNode) {
+ UnitNode unit = (UnitNode) node;
+
+ // ConstantSet?
+ ConstantSet set = unit.getConstantSet();
+
+ if (set != null) {
+ sb.append(String.format("%n%s", set.dumpOf()));
+ }
+ }
+ }
+
+ logger.info(sb.toString());
+ }
+
+ //---------//
+ // getNode //
+ //---------//
+ /**
+ * Retrieves a node object, knowing its path name.
+ *
+ * @param path fully qualified node name
+ * @return the node object, or null if not found
+ */
+ public Node getNode (String path)
+ {
+ return mapOfNodes.get(path);
+ }
+
+ //---------//
+ // getRoot //
+ //---------//
+ /**
+ * Return the PackageNode at the root of the node hierarchy.
+ *
+ * @return the root PackageNode
+ */
+ public PackageNode getRoot ()
+ {
+ return root;
+ }
+
+ //--------------//
+ // preLoadUnits //
+ //--------------//
+ /**
+ * Allows to pre-load the names of the various nodes in the
+ * hierarchy, by simply extracting names stored at previous runs.
+ * This will load the classes not already loaded.
+ * This method is meant to be used by the UI which let the user browse and
+ * modify the whole collection of constants.
+ *
+ * @param main the application main class name
+ */
+ public void preLoadUnits (String main)
+ {
+ //log("pre-loading units");
+ String unitName;
+
+ if (main != null) {
+ unitName = main + ".Units";
+ } else {
+ unitName = "Units";
+ }
+
+ units = new Constant.String(
+ UNIT,
+ unitName,
+ "",
+ "List of units known as containing a ConstantSet");
+
+ // Initialize units using the constant 'units'
+ final String[] tokens = units.getValue().split(SEPARATOR);
+
+ storeIt = false;
+
+ for (String unit : tokens) {
+ try {
+ ///System.out.println ("pre-loading '" + unit + "'...");
+ Class.forName(unit); // This loads its ConstantSet
+ //log ("unit '" + unit + "' pre-loaded");
+ } catch (ClassNotFoundException ex) {
+ System.err.println(
+ "*** Cannot load ConstantSet " + unit + " " + ex);
+ }
+ }
+
+ storeIt = true;
+
+ // Save the latest set of Units
+ storeUnits();
+
+ //log("all units have been pre-loaded from " + main);
+ }
+
+ //-------------//
+ // searchUnits //
+ //-------------//
+ /**
+ * Search for all the units for which the provided string is found
+ * in the unit name or the unit description.
+ *
+ * @param string the string to search for
+ * @return the set (perhaps empty) of the matching units, a mix of UnitNode
+ * and Constant instances.
+ */
+ public Set searchUnits (String string)
+ {
+ String str = string.toLowerCase(Locale.ENGLISH);
+ Set found = new LinkedHashSet<>();
+
+ for (Node node : mapOfNodes.values()) {
+ if (node instanceof UnitNode) {
+ UnitNode unit = (UnitNode) node;
+
+ // Search in unit name itself
+ if (unit.getSimpleName().toLowerCase(Locale.ENGLISH).contains(
+ str)) {
+ found.add(unit);
+ }
+
+ // Search in unit constants, if any
+ ConstantSet set = unit.getConstantSet();
+
+ if (set != null) {
+ for (int i = 0; i < set.size(); i++) {
+ Constant constant = set.getConstant(i);
+
+ if (constant.getName().toLowerCase().contains(str)
+ || constant.getDescription().toLowerCase().
+ contains(str)) {
+ found.add(constant);
+ }
+ }
+ }
+ }
+ }
+
+ return found;
+ }
+
+ //---------//
+ // addUnit //
+ //---------//
+ /**
+ * Include a Unit in the hierarchy.
+ *
+ * @param unit the Unit to include
+ */
+ private void addUnit (UnitNode unit)
+ {
+ //log ("addUnit unit=" + unit.getName());
+ // Update the hierarchy. Include it in the map, as well as all needed
+ // intermediate package nodes if any is needed.
+ String name = unit.getName();
+
+ // Add this node and its parents as needed
+ if (mapOfNodes.putIfAbsent(name, unit) == null) {
+ updateParents(unit);
+
+ if (storeIt) {
+ // Make this unit name permanent
+ storeUnits();
+ }
+ }
+ }
+
+ //-------------//
+ // dumpStrings //
+ //-------------//
+ private void dumpStrings (String title,
+ Collection strings)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(String.format("%s:%n", title));
+
+ for (String string : strings) {
+ sb.append(String.format("%s%n", string));
+ }
+
+ logger.info(sb.toString());
+ }
+
+ //--------------//
+ // retrieveUnit //
+ //--------------//
+ /**
+ * Looks for the unit with given name.
+ * If the unit does not exist, it is created and inserted in the hierarchy.
+ *
+ * @param name the name of the desired unit
+ * @return the unit (found, or created)
+ */
+ private UnitNode retrieveUnit (String name)
+ {
+ Node node = getNode(name);
+
+ if (node == null) {
+ // Create a hosting unit node
+ UnitNode unit = new UnitNode(name);
+ addUnit(unit);
+
+ return unit;
+ } else if (node instanceof UnitNode) {
+ return (UnitNode) node;
+ } else if (node instanceof PackageNode) {
+ logger.error("Unit with same name as package {}", name);
+ }
+
+ return null;
+ }
+
+ //------------//
+ // storeUnits //
+ //------------//
+ /**
+ * Build a string by concatenating all node names and store it to
+ * disk for subsequent runs.
+ */
+ private void storeUnits ()
+ {
+ //log("storing units");
+
+ // Update the constant 'units' according to current units content
+ StringBuilder buf = new StringBuilder(1024);
+
+ for (String name : mapOfNodes.keySet()) {
+ Node node = getNode(name);
+
+ if (node instanceof UnitNode) {
+ if (buf.length() > 0) {
+ buf.append(SEPARATOR);
+ }
+
+ buf.append(name);
+ }
+ }
+
+ // Side-effect: all constants are stored to disk
+ units.setValue(buf.toString());
+
+ //log(units.getName() + "=" + units.getValue());
+ }
+
+ //---------------//
+ // updateParents //
+ //---------------//
+ /**
+ * Update the chain of parents of a Unit, by walking up all the
+ * package names found in the fully qualified Unit name,
+ * creating PackageNodes when needed, or adding a new child to an
+ * existing PackageNode.
+ *
+ * @param unit the Unit whose chain of parents is to be updated
+ */
+ private void updateParents (UnitNode unit)
+ {
+ String name = unit.getName();
+ int length = name.length();
+ Node child = unit;
+
+ for (int i = name.lastIndexOf('.', length - 1); i >= 0;
+ i = name.lastIndexOf('.', i - 1)) {
+ String parent = name.substring(0, i);
+ Node obj = mapOfNodes.get(parent);
+
+ // Create a provision node for a future parent.
+ if (obj == null) {
+ //log("No parent " + sub + " found. Creating PackageNode.");
+ PackageNode pn = new PackageNode(parent, child);
+
+ if (mapOfNodes.putIfAbsent(parent, pn) != null) {
+ // Already done by someone else, give up
+ return;
+ }
+
+ child = pn;
+ } else if (obj instanceof PackageNode) {
+ //log("PackageNode " + sub + " found. adding child");
+ ((PackageNode) obj).addChild(child);
+
+ return;
+ } else {
+ Exception e = new IllegalStateException(
+ "unexpected node type " + obj.getClass() + " in map.");
+ e.printStackTrace();
+
+ return;
+ }
+ }
+
+ // No intermediate parent found, so hook it to the root itself
+ getRoot().addChild(child);
+ }
+
+ //---------------//
+ // resetAllUnits //
+ //---------------//
+ /**
+ * Reset all constants to their factory (source) value.
+ */
+ public void resetAllUnits ()
+ {
+ for (Node node : mapOfNodes.values()) {
+ if (node instanceof UnitNode) {
+ UnitNode unit = (UnitNode) node;
+ ConstantSet set = unit.getConstantSet();
+
+ if (set != null) {
+ for (int i = 0; i < set.size(); i++) {
+ Constant constant = set.getConstant(i);
+ constant.reset();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/omr/constant/UnitModel.java b/src/main/omr/constant/UnitModel.java
new file mode 100644
index 0000000..bf2a2c4
--- /dev/null
+++ b/src/main/omr/constant/UnitModel.java
@@ -0,0 +1,498 @@
+//----------------------------------------------------------------------------//
+// //
+// U n i t M o d e l //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import omr.sheet.Scale;
+import omr.sheet.Sheet;
+import omr.sheet.ui.SheetsController;
+
+import omr.ui.treetable.AbstractTreeTableModel;
+import omr.ui.treetable.TreeTableModel;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.JOptionPane;
+
+/**
+ * Class {@code UnitModel} implements a data model for units suitable
+ * for use in a JTreeTable.
+ *
+ * A row in the UnitModel can be any instance of the 3 following types:
+ *
+ *
+ * PackageNode to represent a package. Its children rows can be
+ * either (sub) PackageNodes or UnitNodes.
+ *
+ * UnitNode to represent a class that contains a ConstantSet.
+ * Its parent node is a PackageNode. Its children rows (if any)
+ * are the Constants of its ConstantSet.
+ *
+ * Constant to represent a constant within a ConstantSet. In that
+ * case, its parent node is a UnitNode. It has no children rows.
+ *
+ *
+ * @author Hervé Bitteur
+ */
+public class UnitModel
+ extends AbstractTreeTableModel
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ UnitModel.class);
+
+ //~ Enumerations -----------------------------------------------------------
+ /**
+ * Enumeration type to describe each column of the JTreeTable
+ */
+ public static enum Column
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ /**
+ * The left column, assigned to tree structure, allows expansion
+ * and collapsing of sub-tree portions.
+ */
+ TREE("Unit", true, 280, TreeTableModel.class),
+ /**
+ * Editable column with modification flag if node is a constant.
+ * Empty if node is a package.
+ */
+ MODIF("Modif", true, 50, String.class),
+ /**
+ * Column that recalls the constant type, and thus the possible
+ * range of values.
+ */
+ TYPE("Type", false, 70, String.class),
+ /**
+ * Column for the units, if any, used for the value.
+ */
+ UNIT("Unit", false, 70, String.class),
+ /**
+ * Column relevant only for constants which are fractions of
+ * interline, as defined by {@link omr.sheet.Scale.Fraction}.
+ * The equivalent number of pixels is displayed, according to the scale
+ * of the currently selected Sheet.
+ * If there is no current Sheet, then just a question mark (?) is shown
+ */
+ PIXEL("Pixels", false, 30, String.class),
+ /**
+ * Editable column for constant current value, with related tool
+ * tip retrieved from the constant declaration.
+ */
+ VALUE("Value", true, 100, String.class),
+ /**
+ * Column dedicated to constant description.
+ */
+ DESC("Description", false, 350, String.class);
+ //~ Instance fields ----------------------------------------------------
+
+ /** Java class to handle column content. */
+ private final Class> type;
+
+ /** Is this column user editable?. */
+ private final boolean editable;
+
+ /** Header for the column. */
+ private final String header;
+
+ /** Width for column display. */
+ private final int width;
+
+ //~ Constructors -------------------------------------------------------
+ //--------//
+ // Column //
+ //--------//
+ Column (String header,
+ boolean editable,
+ int width,
+ Class> type)
+ {
+ this.header = header;
+ this.editable = editable;
+ this.width = width;
+ this.type = type;
+ }
+
+ //~ Methods ------------------------------------------------------------
+ //----------//
+ // getWidth //
+ //----------//
+ public int getWidth ()
+ {
+ return width;
+ }
+ }
+
+ //~ Constructors -----------------------------------------------------------
+ //-----------//
+ // UnitModel //
+ //-----------//
+ /**
+ * Builds the model.
+ */
+ public UnitModel ()
+ {
+ super(UnitManager.getInstance().getRoot());
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------//
+ // getChild //
+ //----------//
+ /**
+ * Returns the child of {@code parent} at index {@code index} in
+ * the parent's child array.
+ *
+ * @param parent a node in the tree, obtained from this data source
+ * @param i the child index in parent sequence
+ * @return the child of {@code parent at index index}
+ */
+ @Override
+ public Object getChild (Object parent,
+ int i)
+ {
+ if (parent instanceof PackageNode) {
+ PackageNode pNode = (PackageNode) parent;
+
+ return pNode.getChild(i);
+ }
+
+ if (parent instanceof UnitNode) {
+ UnitNode unit = (UnitNode) parent;
+ ConstantSet set = unit.getConstantSet();
+
+ if (set != null) {
+ return set.getConstant(i);
+ }
+ }
+
+ System.err.println(
+ "*** getChild. Unexpected node " + parent + ", type="
+ + parent.getClass().getName());
+
+ return null;
+ }
+
+ //---------------//
+ // getChildCount //
+ //---------------//
+ /**
+ * Returns the number of children of {@code parent}.
+ *
+ * @param parent a node in the tree, obtained from this data source
+ * @return the number of children of the node {@code parent}
+ */
+ @Override
+ public int getChildCount (Object parent)
+ {
+ if (parent instanceof PackageNode) {
+ return ((PackageNode) parent).getChildCount();
+ }
+
+ if (parent instanceof UnitNode) {
+ UnitNode unit = (UnitNode) parent;
+ ConstantSet set = unit.getConstantSet();
+
+ return set.size();
+ }
+
+ if (parent instanceof Constant) {
+ return 0;
+ }
+
+ System.err.println(
+ "*** getChildCount. Unexpected node " + parent + ", type="
+ + parent.getClass().getName());
+
+ return 0;
+ }
+
+ //----------------//
+ // getColumnClass //
+ //----------------//
+ /**
+ * Report the class for instances in the provided column.
+ *
+ * @param column the desired column
+ * @return the class for all cells in this column
+ */
+ @Override
+ public Class> getColumnClass (int column)
+ {
+ return Column.values()[column].type;
+ }
+
+ //----------------//
+ // getColumnCount //
+ //----------------//
+ /**
+ * Report the number of column in the table.
+ *
+ * @return the table number of columns
+ */
+ @Override
+ public int getColumnCount ()
+ {
+ return Column.values().length;
+ }
+
+ //---------------//
+ // getColumnName //
+ //---------------//
+ /**
+ * Report the name of the column at hand.
+ *
+ * @param column the desired column
+ * @return the column name
+ */
+ @Override
+ public String getColumnName (int column)
+ {
+ return Column.values()[column].header;
+ }
+
+ //------------//
+ // getValueAt //
+ //------------//
+ /**
+ * Report the value of a cell.
+ *
+ * @param node the desired node
+ * @param col the related column
+ * @return the cell value
+ */
+ @Override
+ public Object getValueAt (Object node,
+ int col)
+ {
+ Column column = Column.values()[col];
+
+ switch (column) {
+ case MODIF:
+
+ if (node instanceof PackageNode) {
+ return null;
+ } else if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ return Boolean.valueOf(!constant.isSourceValue());
+ }
+
+ return "";
+
+ case VALUE:
+
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ if (constant instanceof Constant.Boolean) {
+ Constant.Boolean cb = (Constant.Boolean) constant;
+
+ return cb.getCachedValue();
+ } else {
+ return constant.getCurrentString();
+ }
+ } else {
+ return "";
+ }
+
+ case TYPE:
+
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ return constant.getShortTypeName();
+ } else {
+ return "";
+ }
+
+ case UNIT:
+
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ return (constant.getQuantityUnit() != null)
+ ? constant.getQuantityUnit() : "";
+ } else {
+ return "";
+ }
+
+ case PIXEL:
+
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ if (constant instanceof Scale.Fraction
+ || constant instanceof Scale.LineFraction
+ || constant instanceof Scale.AreaFraction) {
+ // Compute the equivalent in pixels of this interline-based
+ // fraction, line or area fraction, provided that we have a
+ // current sheet and its scale is available.
+ Sheet sheet = SheetsController.getCurrentSheet();
+
+ if (sheet != null) {
+ Scale scale = sheet.getScale();
+
+ if (scale != null) {
+ if (constant instanceof Scale.Fraction) {
+ return Integer.valueOf(
+ scale.toPixels((Scale.Fraction) constant));
+ } else if (constant instanceof Scale.LineFraction) {
+ return Integer.valueOf(
+ scale.toPixels(
+ (Scale.LineFraction) constant));
+ } else if (constant instanceof Scale.AreaFraction) {
+ return Integer.valueOf(
+ scale.toPixels(
+ (Scale.AreaFraction) constant));
+ }
+ }
+ } else {
+ return "?"; // Cannot compute the value
+ }
+ }
+ }
+
+ return "";
+
+ case DESC:
+
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ return constant.getDescription();
+ } else {
+ return null;
+ }
+ }
+
+ return null; // For the compiler
+ }
+
+ //----------------//
+ // isCellEditable //
+ //----------------//
+ /**
+ * Predicate on cell being editable
+ *
+ * @param node the related tree node
+ * @param column the related table column
+ * @return true if editable
+ */
+ @Override
+ public boolean isCellEditable (Object node,
+ int column)
+ {
+ Column col = Column.values()[column];
+
+ if (col == Column.MODIF) {
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+
+ return Boolean.valueOf(!constant.isSourceValue());
+
+ // } else if (node instanceof UnitNode) {
+ // return true;
+ } else {
+ return false;
+ }
+ } else {
+ return col.editable;
+ }
+ }
+
+ //--------//
+ // isLeaf //
+ //--------//
+ /**
+ * Returns {@code true} if {@code node} is a leaf.
+ *
+ * @param node a node in the tree, obtained from this data source
+ * @return true if {@code node} is a leaf
+ */
+ @Override
+ public boolean isLeaf (Object node)
+ {
+ if (node instanceof Constant) {
+ return true;
+ }
+
+ if (node instanceof UnitNode) {
+ UnitNode unit = (UnitNode) node;
+
+ return (unit.getConstantSet() == null);
+ }
+
+ return false; // By default
+ }
+
+ //------------//
+ // setValueAt //
+ //------------//
+ /**
+ * Assign a value to a cell
+ *
+ * @param value the value to assign
+ * @param node the target node
+ * @param col the related column
+ */
+ @Override
+ public void setValueAt (Object value,
+ Object node,
+ int col)
+ {
+ if (node instanceof Constant) {
+ Constant constant = (Constant) node;
+ Column column = Column.values()[col];
+
+ switch (column) {
+ case VALUE:
+
+ try {
+ constant.setValue(value.toString());
+
+ // Forward modif to the modif status column and to the pixel
+ // column (brute force!)
+ fireTreeNodesChanged(
+ this,
+ new Object[]{getRoot()},
+ null,
+ null);
+ } catch (NumberFormatException ex) {
+ JOptionPane.showMessageDialog(
+ null,
+ "Illegal number format");
+ }
+
+ break;
+
+ case MODIF:
+
+ if (!((Boolean) value).booleanValue()) {
+ constant.reset();
+ fireTreeNodesChanged(
+ this,
+ new Object[]{getRoot()},
+ null,
+ null);
+ }
+
+ break;
+
+ default:
+ }
+ }
+ }
+}
diff --git a/src/main/omr/constant/UnitNode.java b/src/main/omr/constant/UnitNode.java
new file mode 100644
index 0000000..d0340f3
--- /dev/null
+++ b/src/main/omr/constant/UnitNode.java
@@ -0,0 +1,96 @@
+//----------------------------------------------------------------------------//
+// //
+// U n i t N o d e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import org.slf4j.Logger;
+/**
+ * Class {@code UnitNode} represents a unit (class) in the hierarchy of
+ * nodes.
+ * It represents a class and can have either a Logger, a ConstantSet, or both.
+ *
+ * @author Hervé Bitteur
+ */
+public class UnitNode
+ extends Node
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /** The contained Constant set if any */
+ private ConstantSet set;
+
+ /** The logger if any */
+ private Logger logger;
+
+ //~ Constructors -----------------------------------------------------------
+
+ //----------//
+ // UnitNode //
+ //----------//
+ /**
+ * Create a new UnitNode.
+ * @param name the fully qualified class/unit name
+ */
+ public UnitNode (String name)
+ {
+ super(name);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ //----------------//
+ // getConstantSet //
+ //----------------//
+ /**
+ * Retrieves the ConstantSet associated to the unit (if any).
+ * @return the ConstantSet instance, or null
+ */
+ public ConstantSet getConstantSet ()
+ {
+ return set;
+ }
+
+ //-----------//
+ // getLogger //
+ //-----------//
+ /**
+ * Retrieves the Logger instance associated to the unit (if any).
+ * @return the Logger instance, or null
+ */
+ public Logger getLogger ()
+ {
+ return logger;
+ }
+
+ //----------------//
+ // setConstantSet //
+ //----------------//
+ /**
+ * Assigns the provided ConstantSet to this enclosing unit.
+ * @param set the ConstantSet to be assigned
+ */
+ public void setConstantSet (ConstantSet set)
+ {
+ this.set = set;
+ }
+
+ //-----------//
+ // setLogger //
+ //-----------//
+ /**
+ * Assigns the provided Logger to the unit.
+ * @param logger the Logger instance
+ */
+ public void setLogger (Logger logger)
+ {
+ this.logger = logger;
+ }
+}
diff --git a/src/main/omr/constant/UnitTreeTable.java b/src/main/omr/constant/UnitTreeTable.java
new file mode 100644
index 0000000..fc1736a
--- /dev/null
+++ b/src/main/omr/constant/UnitTreeTable.java
@@ -0,0 +1,392 @@
+//----------------------------------------------------------------------------//
+// //
+// U n i t T r e e T a b l e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.constant;
+
+import omr.ui.treetable.JTreeTable;
+import omr.ui.treetable.TreeTableModelAdapter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.JTable;
+import javax.swing.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumnModel;
+import javax.swing.tree.TreePath;
+
+/**
+ * Class {@code UnitTreeTable} is a user interface that combines a tree
+ * to display the hierarchy of Units, that contains ConstantSets,
+ * and a table to display and edit the various Constants in
+ * each ConstantSet.
+ *
+ * @author Hervé Bitteur
+ */
+public class UnitTreeTable
+ extends JTreeTable
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(UnitTreeTable.class);
+
+ /** Alternate color for zebra appearance */
+ private static final Color zebraColor = new Color(248, 248, 255);
+
+ //~ Instance fields --------------------------------------------------------
+ private TableCellRenderer valueRenderer = new ValueRenderer();
+
+ private TableCellRenderer pixelRenderer = new PixelRenderer();
+
+ //~ Constructors -----------------------------------------------------------
+ //---------------//
+ // UnitTreeTable //
+ //---------------//
+ /**
+ * Create a User Interface JTreeTable dedicated to the handling of
+ * unit constants.
+ *
+ * @param model the corresponding data model
+ */
+ public UnitTreeTable (UnitModel model)
+ {
+ super(model);
+
+ setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+
+ // Zebra
+ UIManager.put("Table.alternateRowColor", zebraColor);
+ setFillsViewportHeight(true);
+
+ // Show grid?
+ //setShowGrid(true);
+
+ // Specify column widths
+ adjustColumns();
+
+ // Customize the tree aspect
+ tree.setRootVisible(false);
+ tree.setShowsRootHandles(true);
+
+ // Pre-expand all package nodes
+ preExpandPackages();
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //---------------//
+ // getCellEditor //
+ //---------------//
+ @Override
+ public TableCellEditor getCellEditor (int row,
+ int col)
+ {
+ UnitModel.Column column = UnitModel.Column.values()[col];
+
+ switch (column) {
+ case MODIF: {
+ Object node = nodeForRow(row);
+
+ if (node instanceof Constant) {
+ return getDefaultEditor(Boolean.class);
+ }
+ }
+
+ break;
+
+ case VALUE: {
+ Object obj = getModel().getValueAt(row, col);
+
+ if (obj instanceof Boolean) {
+ return getDefaultEditor(Boolean.class);
+ }
+ }
+
+ break;
+
+ default:
+ }
+
+ // Default cell editor (determined by column class)
+ return super.getCellEditor(row, col);
+ }
+
+ //-----------------//
+ // getCellRenderer //
+ //-----------------//
+ /**
+ * Used by the UI to get the proper renderer for each given cell in
+ * the table.
+ *
+ * @param row row in the table
+ * @param col column in the table
+ * @return the best renderer for the cell.
+ */
+ @Override
+ public TableCellRenderer getCellRenderer (int row,
+ int col)
+ {
+ UnitModel.Column column = UnitModel.Column.values()[col];
+
+ switch (column) {
+ case MODIF: {
+ Object obj = getModel().getValueAt(row, col);
+
+ if (obj instanceof Boolean) {
+ // A constant => Modif flag
+ return getDefaultRenderer(Boolean.class);
+ } else {
+ // A node (unit or package)
+ return getDefaultRenderer(Object.class);
+ }
+ }
+
+ case VALUE: {
+ Object obj = getModel().getValueAt(row, col);
+
+ if (obj instanceof Boolean) {
+ return getDefaultRenderer(Boolean.class);
+ } else {
+ return valueRenderer;
+ }
+ }
+
+ case PIXEL:
+ return pixelRenderer;
+
+ default:
+ return getDefaultRenderer(getColumnClass(col));
+ }
+ }
+
+ //--------------------//
+ // scrollRowToVisible //
+ //--------------------//
+ /**
+ * Scroll so that the provided row gets visible
+ *
+ * @param row the provided row
+ */
+ public void scrollRowToVisible (int row)
+ {
+ final int height = tree.getRowHeight();
+ Rectangle rect = new Rectangle(0, row * height, 0, 0);
+
+ if (getParent() instanceof JComponent) {
+ JComponent comp = (JComponent) getParent();
+ rect.grow(0, comp.getHeight() / 2);
+ } else {
+ rect.grow(0, height);
+ }
+
+ scrollRectToVisible(rect);
+ }
+
+ //-------------------//
+ // setNodesSelection //
+ //-------------------//
+ /**
+ * Select the rows that correspond to the provided nodes
+ *
+ * @param matches the nodes to select
+ * @return the relevant rows
+ */
+ public List setNodesSelection (Collection matches)
+ {
+ List paths = new ArrayList<>();
+
+ for (Object object : matches) {
+ if (object instanceof Constant) {
+ Constant constant = (Constant) object;
+ TreePath path = getPath(constant, constant.getQualifiedName());
+ paths.add(path);
+ } else if (object instanceof Node) {
+ Node node = (Node) object;
+ TreePath path = getPath(node, node.getName());
+ paths.add(path);
+ }
+ }
+
+ // Selection on tree side
+ tree.setSelectionPaths(paths.toArray(new TreePath[paths.size()]));
+
+ // Selection on table side
+ clearSelection();
+
+ List rows = new ArrayList<>();
+
+ for (TreePath path : paths) {
+ int row = tree.getRowForPath(path);
+
+ if (row != -1) {
+ rows.add(row);
+ addRowSelectionInterval(row, row);
+ }
+ }
+
+ Collections.sort(rows);
+
+ return rows;
+ }
+
+ //---------------//
+ // adjustColumns //
+ //---------------//
+ /**
+ * Allows to adjust the related columnModel, for each and every
+ * column
+ *
+ * @param cModel the proper table column model
+ */
+ private void adjustColumns ()
+ {
+ TableColumnModel cModel = getColumnModel();
+
+ // Columns widths
+ for (UnitModel.Column c : UnitModel.Column.values()) {
+ cModel.getColumn(c.ordinal()).setPreferredWidth(c.getWidth());
+ }
+ }
+
+ //---------//
+ // getPath //
+ //---------//
+ private TreePath getPath (Object object,
+ String fullName)
+ {
+ UnitManager unitManager = UnitManager.getInstance();
+ List objects = new ArrayList<>();
+ objects.add(unitManager.getRoot());
+
+ int dotPos = -1;
+
+ while ((dotPos = fullName.indexOf('.', dotPos + 1)) != -1) {
+ String path = fullName.substring(0, dotPos);
+ objects.add(unitManager.getNode(path));
+ }
+
+ objects.add(object);
+
+ logger.debug("path to {} objects:{}", fullName, objects);
+
+ return new TreePath(objects.toArray());
+ }
+
+ //------------//
+ // nodeForRow //
+ //------------//
+ /**
+ * Return the tree node facing the provided table row
+ *
+ * @param row the provided row
+ * @return the corresponding tree node
+ */
+ private Object nodeForRow (int row)
+ {
+ return ((TreeTableModelAdapter) getModel()).nodeForRow(row);
+ }
+
+ //-------------------//
+ // preExpandPackages //
+ //-------------------//
+ /**
+ * Before displaying the tree, expand all nodes that correspond to
+ * packages (PackageNode).
+ */
+ private void preExpandPackages ()
+ {
+ for (int row = 0; row < tree.getRowCount(); row++) {
+ if (tree.isCollapsed(row)) {
+ tree.expandRow(row);
+ }
+ }
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+
+ //---------------//
+ // PixelRenderer //
+ //---------------//
+ private static class PixelRenderer
+ extends DefaultTableCellRenderer
+ {
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public Component getTableCellRendererComponent (JTable table,
+ Object value,
+ boolean isSelected,
+ boolean hasFocus,
+ int row,
+ int column)
+ {
+ super.getTableCellRendererComponent(
+ table,
+ value,
+ isSelected,
+ hasFocus,
+ row,
+ column);
+
+ // Use right alignment
+ setHorizontalAlignment(SwingConstants.RIGHT);
+
+ return this;
+ }
+ }
+
+ //---------------//
+ // ValueRenderer //
+ //---------------//
+ private static class ValueRenderer
+ extends DefaultTableCellRenderer
+ {
+ //~ Methods ------------------------------------------------------------
+
+ @Override
+ public Component getTableCellRendererComponent (JTable table,
+ Object value,
+ boolean isSelected,
+ boolean hasFocus,
+ int row,
+ int column)
+ {
+ super.getTableCellRendererComponent(
+ table,
+ value,
+ isSelected,
+ hasFocus,
+ row,
+ column);
+
+ // Use a bold font
+ setFont(table.getFont().deriveFont(Font.BOLD).deriveFont(12.0f));
+
+ // Use center alignment
+ setHorizontalAlignment(SwingConstants.CENTER);
+
+ return this;
+ }
+ }
+}
diff --git a/src/main/omr/constant/doc-files/Constant.uxf b/src/main/omr/constant/doc-files/Constant.uxf
new file mode 100644
index 0000000..db74c18
--- /dev/null
+++ b/src/main/omr/constant/doc-files/Constant.uxf
@@ -0,0 +1,30 @@
+//Uncomment the following line to change the fontsize:
+//fontsize=14
+
+//Welcome to UMLet!
+
+// *Double-click on UML elements to add them to the diagram.
+// *Edit element properties by modifying the text in this panel.
+// *Edit the files in the 'palettes' directory to store your own element palettes.
+// *Press Del or Backspace to remove elements from the diagram.
+// *Hold down Ctrl key to select multiple elements.
+// *Press c to copy the UML diagram to the system clipboard.
+// * This text will be stored with each diagram. Feel free to use the area for notes.
+ com.umlet.element.custom.Database 600 220 120 50 User Properties com.umlet.element.custom.Database 600 290 120 50 Default Properties com.umlet.element.base.Relation 450 370 170 40 lt=<[<] - 20;20;150;20 com.umlet.element.custom.Database 600 360 120 50 Source Code com.umlet.element.base.Relation 530 260 90 80 lt=<[<] - 20;20;70;60 com.umlet.element.base.Relation 530 230 90 40 lt=<[<] - [>]> 20;20;70;20 com.umlet.element.base.Relation 420 280 40 80 lt=<<<- 20;20;20;60 com.umlet.element.base.Class 410 230 140 70 ConstantManager
+ com.umlet.element.base.Class 410 30 100 40 UnitTreeTable com.umlet.element.base.Relation 240 20 190 40 lt=<- 20;20;170;20 com.umlet.element.base.Class 160 30 100 30 UnitManager com.umlet.element.base.Relation 340 260 40 100 lt=<<<-> 20;20;20;80 com.umlet.element.base.Class 330 340 140 190 Constant
+--
+String quantityUnit
+String defaultString
+String description
+--
+String unit
+String name
+String qualifiedName
+--
+String initialString
+String currentString
+Object cachedValue com.umlet.element.base.Class 290 230 100 50 ConstantSet
+--
+String unit com.umlet.element.base.Class 290 170 70 40 Logger
+--
+level com.umlet.element.base.Relation 60 110 140 120 lt=<<<-> 20;100;20;20;120;20 com.umlet.element.base.Class 20 210 100 30 PackageNode com.umlet.element.base.Relation 190 40 40 100 lt=<<<<-> 20;20;20;80 com.umlet.element.base.Relation 100 130 130 110 lt=<<- 110;20;20;90 com.umlet.element.base.Class 170 210 70 30 UnitNode com.umlet.element.base.Relation 190 130 40 100 lt=<<- 20;20;20;80 com.umlet.element.base.Relation 220 180 90 60 lt=<- 70;20;20;40 com.umlet.element.base.Class 180 120 60 30 /Node/ com.umlet.element.base.Relation 220 200 90 60 lt=<- 70;40;20;20
\ No newline at end of file
diff --git a/src/main/omr/constant/package.html b/src/main/omr/constant/package.html
new file mode 100644
index 0000000..9dec81b
--- /dev/null
+++ b/src/main/omr/constant/package.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ Package omr.constant
+
+
+
+
+ The purpose of this package is to handle application logical
+ constants in a common way, from their definition in their hosting
+ classes, their potential on-line modification, and their
+ persistency on disk. If one day Audiveris migrates to NetBeans
+ platform, the bulk of this package should disappear.
+
+
+
+
+
+
+ Constant instances represent a logical
+ application constant, whose persistency is to be managed from one
+ application run to the other.
+
+
+ A constant (and its subclasses) can be:
+
+
+
+ A Constant declaration enclosed in a ConstantSet instance. This is by far the
+ most common and easiest way to define constants related to specific
+ application class.
+
+ A standalone Constant defined outside the scope of any
+ ConstantSet. This case is typically used when the name of the
+ constant must be forged programmatically.
+
+
+
+ Modification of a Constant value must preferably be
+ performed through dedicated interfaces. You can directly edit the
+ property files, but you do so at your own risks. There are two
+ constant-related GUI within the application:
+
+
+
+ The UnitTreeTable is the GUI
+ related to the UnitManager singleton
+ which handles all the units (classes) for which there is either a
+ Logger instance or a ConstantSet instance, or both. This GUI is
+ launched from Tools | Options menu and handles the tree of these
+ units.
+
+ The ShapeColorChooser is a
+ rudimentary GUI interface to set the color of each and every glyph
+ shape.
+
+
+
+
+ Persistency of each constant is handled by the ConstantManager singleton, which uses
+ the qualified name of the constant as the key to retrieve a related
+ property (if any) specified in either the DEFAULT and/or the USER
+ property files. Please refer to the ConstantManager documentation to read
+ the details on how a constant value is determined.
+ Since the persistency of a Constant uses its fully qualified name
+ (i.e. the path to the enclosing class, plus the name of the
+ constant element in the ConstantSet), the determination of the
+ fully qualified name is deferred until the value of the Constant is
+ actually retrieved. This is implemented through the use of a
+ DirtySet within the UnitManager.
+
+
+
+
diff --git a/src/main/omr/glyph/AbstractEvaluationEngine.java b/src/main/omr/glyph/AbstractEvaluationEngine.java
new file mode 100644
index 0000000..cba0896
--- /dev/null
+++ b/src/main/omr/glyph/AbstractEvaluationEngine.java
@@ -0,0 +1,459 @@
+//----------------------------------------------------------------------------//
+// //
+// A b s t r a c t G l y p h E v a l u a t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.WellKnowns;
+
+import omr.constant.ConstantSet;
+
+import omr.glyph.ShapeEvaluator.Condition;
+import static omr.glyph.ShapeEvaluator.Condition.*;
+import omr.glyph.facets.Glyph;
+
+import omr.sheet.Scale;
+import omr.sheet.SystemInfo;
+
+import omr.util.Predicate;
+import omr.util.UriUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+import javax.xml.bind.JAXBException;
+
+/**
+ * Class {@code AbstractEvaluationEngine} is an abstract implementation
+ * for any evaluation engine.
+ *
+ *
+ *
+ * @author Hervé Bitteur
+ */
+public abstract class AbstractEvaluationEngine
+ implements EvaluationEngine
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ AbstractEvaluationEngine.class);
+
+ /** Number of shapes to differentiate. */
+ protected static final int shapeCount = 1
+ + Shape.LAST_PHYSICAL_SHAPE.ordinal();
+
+ /** A special evaluation array, used to report NOISE. */
+ protected static final Evaluation[] noiseEvaluations = {
+ new Evaluation(Shape.NOISE, Evaluation.ALGORITHM)
+ };
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** The glyph checker for additional specific checks. */
+ protected ShapeChecker glyphChecker = ShapeChecker.getInstance();
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //----------//
+ // evaluate //
+ //----------//
+ @Override
+ public Evaluation[] evaluate (Glyph glyph,
+ SystemInfo system,
+ int count,
+ double minGrade,
+ EnumSet conditions,
+ Predicate predicate)
+ {
+ List best = new ArrayList<>();
+ Evaluation[] evals = getRawEvaluations(glyph);
+
+ EvalsLoop:
+ for (Evaluation eval : evals) {
+ // Bounding test?
+ if ((best.size() >= count) || (eval.grade < minGrade)) {
+ break;
+ }
+
+ // Predicate?
+ if ((predicate != null) && !predicate.check(eval.shape)) {
+ continue;
+ }
+
+ // Allowed?
+ if (conditions.contains(Condition.ALLOWED)
+ && glyph.isShapeForbidden(eval.shape)) {
+ continue;
+ }
+
+ // Successful checks?
+ if (conditions.contains(Condition.CHECKED)) {
+ Evaluation oldEval = new Evaluation(eval.shape, eval.grade);
+ double[] ins = ShapeDescription.features(glyph);
+ // This may change the eval shape...
+ glyphChecker.annotate(system, eval, glyph, ins);
+
+ if (eval.failure != null) {
+ continue;
+ }
+
+ // In case the specific checks have changed eval shape
+ // we have to retest against the glyph blacklist
+ if ((eval.shape != oldEval.shape)
+ && conditions.contains(Condition.ALLOWED)
+ && glyph.isShapeForbidden(eval.shape)) {
+ continue;
+ }
+ }
+
+ // Everything is OK, add the shape if not already in the list
+ for (Evaluation e : best) {
+ if (e.shape == eval.shape) {
+ continue EvalsLoop;
+ }
+ }
+ best.add(eval);
+ }
+
+ return best.toArray(new Evaluation[0]);
+ }
+
+ //-------------//
+ // isBigEnough //
+ //-------------//
+ @Override
+ public boolean isBigEnough (Glyph glyph)
+ {
+ return glyph.getNormalizedWeight() >= constants.minWeight.getValue();
+ }
+
+ //---------//
+ // marshal //
+ //---------//
+ /**
+ * Store the engine in XML format, always as a user file.
+ */
+ @Override
+ public void marshal ()
+ {
+ final File file = new File(WellKnowns.EVAL_FOLDER, getFileName());
+ OutputStream os = null;
+
+ try {
+ os = new FileOutputStream(file);
+ marshal(os);
+ logger.info("Engine marshalled to {}", file);
+ } catch (FileNotFoundException ex) {
+ logger.warn("Could not find file " + file, ex);
+ } catch (IOException ex) {
+ logger.warn("IO error on file " + file, ex);
+ } catch (JAXBException ex) {
+ logger.warn("Error marshalling engine to " + file, ex);
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+ }
+
+ //---------//
+ // rawVote //
+ //---------//
+ @Override
+ public Evaluation rawVote (Glyph glyph,
+ double minGrade,
+ Predicate predicate)
+ {
+ Evaluation[] evals = evaluate(glyph, null, 1, minGrade,
+ EnumSet.of(ALLOWED), predicate);
+
+ if (evals.length > 0) {
+ return evals[0];
+ } else {
+ return null;
+ }
+ }
+
+ //------//
+ // stop //
+ //------//
+ /**
+ * Stop the on-going training.
+ * By default, this is a no-op
+ */
+ @Override
+ public void stop ()
+ {
+ }
+
+ //------//
+ // Vote //
+ //------//
+ @Override
+ public Evaluation vote (Glyph glyph,
+ SystemInfo system,
+ double minGrade,
+ EnumSet conditions,
+ Predicate predicate)
+ {
+ Evaluation[] evals = evaluate(glyph, system, 1, minGrade, conditions,
+ predicate);
+
+ if (evals.length > 0) {
+ return evals[0];
+ } else {
+ return null;
+ }
+ }
+
+ //------//
+ // vote //
+ //------//
+ @Override
+ public Evaluation vote (Glyph glyph,
+ SystemInfo system,
+ double minGrade,
+ Predicate predicate)
+ {
+ Evaluation[] evals = evaluate(glyph, system, 1, minGrade,
+ EnumSet.of(ALLOWED, CHECKED), predicate);
+
+ if (evals.length > 0) {
+ return evals[0];
+ } else {
+ return null;
+ }
+ }
+
+ //------//
+ // vote //
+ //------//
+ @Override
+ public Evaluation vote (Glyph glyph,
+ SystemInfo system,
+ double minGrade)
+ {
+ Evaluation[] evals = evaluate(glyph, system, 1, minGrade,
+ EnumSet.of(ALLOWED, CHECKED), null);
+
+ if (evals.length > 0) {
+ return evals[0];
+ } else {
+ return null;
+ }
+ }
+
+ //-------------//
+ // getFileName //
+ //-------------//
+ /**
+ * Report the simple file name, including extension but excluding
+ * parent, which contains the marshalled data of the evaluator.
+ *
+ * @return the file name
+ */
+ protected abstract String getFileName ();
+
+ //-------------------//
+ // getRawEvaluations //
+ //-------------------//
+ /**
+ * Run the evaluator with the specified glyph, and return a
+ * sequence of interpretations (ordered from best to worst) with
+ * no additional check.
+ *
+ * @param glyph the glyph to be examined
+ * @return the ordered best evaluations
+ */
+ protected abstract Evaluation[] getRawEvaluations (Glyph glyph);
+
+ //---------//
+ // marshal //
+ //---------//
+ protected abstract void marshal (OutputStream os)
+ throws FileNotFoundException, IOException, JAXBException;
+
+ //-----------//
+ // unmarshal //
+ //-----------//
+ /**
+ * The specific unmarshalling method which builds a suitable engine.
+ *
+ * @param is the input stream to read
+ * @return the newly built evaluation engine
+ * @throws JAXBException, IOException
+ */
+ protected abstract Object unmarshal (InputStream is)
+ throws JAXBException, IOException;
+
+ //-----------//
+ // unmarshal //
+ //-----------//
+ /**
+ * Unmarshal the evaluation engine from the most suitable file.
+ * If a user file does not exist or cannot be unmarshalled, the
+ * system default file is used
+ *
+ * @return the unmarshalled engine, or null if everything failed
+ */
+ protected Object unmarshal ()
+ {
+ // First try user file, if any (in user EVAL folder)
+ {
+ File file = new File(WellKnowns.EVAL_FOLDER, getFileName());
+ if (file.exists()) {
+ Object obj = unmarshal(file);
+
+ if (obj == null) {
+ logger.warn("Could not load {}", file);
+ } else {
+ if (!isCompatible(obj)) {
+ final String msg = "Obsolete user data for " + getName()
+ + " in " + file
+ + ", trying default data";
+ logger.warn(msg);
+ JOptionPane.showMessageDialog(null, msg);
+ } else {
+ // Tell the user we are not using the default
+ logger.info("{} unmarshalled from {}", getName(), file);
+ return obj;
+ }
+ }
+ }
+ }
+
+ // Use default file (in program RES folder)
+ //file = new File(WellKnowns.RES_URI, getFileName());
+ URI uri = UriUtil.toURI(WellKnowns.RES_URI, getFileName());
+ InputStream input;
+ try {
+ input = uri.toURL().openStream();
+ } catch (Exception ex) {
+ logger.warn("Error in " + uri, ex);
+ return null;
+ }
+ Object obj = unmarshal(input, getFileName());
+
+ if (obj == null) {
+ logger.warn("Could not load {}", uri);
+ } else {
+ if (!isCompatible(obj)) {
+ final String msg = "Obsolete default data for " + getName()
+ + " in " + uri
+ + ", please retrain from scratch";
+ logger.warn(msg);
+ JOptionPane.showMessageDialog(null, msg);
+
+ obj = null;
+ } else {
+ logger.debug("{} unmarshalled from {}", getName(), uri);
+ }
+ }
+
+ return obj;
+ }
+
+ //--------------//
+ // isCompatible //
+ //--------------//
+ /**
+ * Make sure the provided engine object is compatible with the
+ * current application.
+ *
+ * @param obj the engine object
+ * @return true if engine is usable and found compatible
+ */
+ protected abstract boolean isCompatible (Object obj);
+
+ //-----------//
+ // unmarshal //
+ //-----------//
+ /**
+ * Unmarshal the evaluation engine using provided file.
+ *
+ * @return the unmarshalled engine, or null if failed
+ */
+ private Object unmarshal (File file)
+ {
+ try {
+ InputStream input = new FileInputStream(file);
+
+ return unmarshal(input, getFileName());
+ } catch (FileNotFoundException ex) {
+ logger.warn("File not found " + file, ex);
+
+ return null;
+ } catch (Exception ex) {
+ logger.warn("Error unmarshalling from " + file, ex);
+
+ return null;
+ }
+ }
+
+ //-----------//
+ // unmarshal //
+ //-----------//
+ private Object unmarshal (InputStream is,
+ String name)
+ {
+ if (is == null) {
+ logger.warn("No data stream for {} engine as {}",
+ getName(), name);
+ } else {
+ try {
+ Object engine = unmarshal(is);
+ is.close();
+
+ return engine;
+ } catch (FileNotFoundException ex) {
+ logger.warn("Cannot find or read " + name, ex);
+ } catch (IOException ex) {
+ logger.warn("IO error on " + name, ex);
+ } catch (JAXBException ex) {
+ logger.warn("Error unmarshalling evaluator from " + name, ex);
+ }
+ }
+
+ return null;
+ }
+
+ //-----------//
+ // Constants //
+ //-----------//
+ private static final class Constants
+ extends ConstantSet
+ {
+
+ Scale.AreaFraction minWeight = new Scale.AreaFraction(0.08,
+ "Minimum normalized weight to be considered not a noise");
+
+ }
+}
diff --git a/src/main/omr/glyph/BasicNest.java b/src/main/omr/glyph/BasicNest.java
new file mode 100644
index 0000000..ccf6d8e
--- /dev/null
+++ b/src/main/omr/glyph/BasicNest.java
@@ -0,0 +1,833 @@
+//----------------------------------------------------------------------------//
+// //
+// B a s i c N e s t //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.Main;
+
+import omr.constant.Constant;
+import omr.constant.ConstantSet;
+
+import omr.glyph.facets.Glyph;
+import omr.glyph.ui.ViewParameters;
+
+import omr.lag.BasicRoi;
+import omr.lag.Roi;
+import omr.lag.Section;
+import omr.lag.Sections;
+
+import omr.math.Histogram;
+
+import omr.run.Orientation;
+
+import omr.selection.GlyphEvent;
+import omr.selection.GlyphIdEvent;
+import omr.selection.GlyphSetEvent;
+import omr.selection.LocationEvent;
+import omr.selection.MouseMovement;
+import omr.selection.NestEvent;
+import omr.selection.SelectionHint;
+import static omr.selection.SelectionHint.*;
+import omr.selection.SelectionService;
+import omr.selection.UserEvent;
+
+import omr.sheet.Sheet;
+import omr.sheet.SystemInfo;
+
+import omr.util.VipUtil;
+
+import org.bushe.swing.event.EventSubscriber;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Class {@code BasicNest} implements a {@link Nest}.
+ *
+ * @author Hervé Bitteur
+ */
+public class BasicNest
+ implements Nest,
+ EventSubscriber
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(BasicNest.class);
+
+ /** Events read on location service */
+ public static final Class>[] locEventsRead = new Class>[]{
+ LocationEvent.class};
+
+ /** Events read on nest (glyph) service */
+ public static final Class>[] glyEventsRead = new Class>[]{
+ GlyphIdEvent.class,
+ GlyphEvent.class,
+ GlyphSetEvent.class
+ };
+
+ //~ Instance fields --------------------------------------------------------
+ /** (Debug) a unique name for this nest. */
+ private final String name;
+
+ /** Related sheet. */
+ private final Sheet sheet;
+
+ /** Elaborated constants for this nest. */
+ private final Parameters params;
+
+ /**
+ * Smart glyph map, based on a physical glyph signature, and thus
+ * usable across several glyph extractions, to ensure glyph unicity
+ * whatever the sequential ID it is assigned.
+ */
+ private final ConcurrentHashMap originals = new ConcurrentHashMap<>();
+
+ /**
+ * Collection of all glyphs ever inserted in this Nest, indexed by
+ * glyph id. No non-virtual glyph is ever removed from this map.
+ */
+ private final ConcurrentHashMap allGlyphs = new ConcurrentHashMap<>();
+
+ /**
+ * Current map of section -> glyphs.
+ * This defines the glyphs that are currently active, since there is at
+ * least one section pointing to them (the sections collection is
+ * immutable).
+ * Nota: The glyph reference within the section is kept in sync
+ */
+ private final ConcurrentHashMap activeMap = new ConcurrentHashMap<>();
+
+ /**
+ * Collection of active glyphs.
+ * This is derived from the activeMap, to give direct access to all the
+ * active glyphs, and is kept in sync with activeMap.
+ * It also contains the virtual glyphs since these are always active.
+ */
+ private Set activeGlyphs;
+
+ /** Collection of virtual glyphs. (with no underlying sections) */
+ private Set virtualGlyphs = new HashSet<>();
+
+ /** Global id to uniquely identify a glyph. */
+ private final AtomicInteger globalGlyphId = new AtomicInteger(0);
+
+ /** Location service (read & write). */
+ private SelectionService locationService;
+
+ /** Hosted glyph service. (Glyph, GlyphId and GlyphSet) */
+ protected final SelectionService glyphService;
+
+ //~ Constructors -----------------------------------------------------------
+ //-----------//
+ // BasicNest //
+ //-----------//
+ /**
+ * Create a glyph nest.
+ *
+ * @param name the distinguished name for this instance
+ */
+ public BasicNest (String name,
+ Sheet sheet)
+ {
+ this.name = name;
+ this.sheet = sheet;
+
+ params = new Parameters();
+ glyphService = new SelectionService(name, Nest.eventsWritten);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------//
+ // addGlyph //
+ //----------//
+ @Override
+ public Glyph addGlyph (Glyph glyph)
+ {
+ glyph = registerGlyph(glyph);
+
+ // Make absolutely all its sections point back to it
+ glyph.linkAllSections();
+
+ if (glyph.isVip()) {
+ logger.info("{} added", glyph.idString());
+ }
+
+ return glyph;
+ }
+
+ //--------//
+ // dumpOf //
+ //--------//
+ @Override
+ public String dumpOf (String title)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if (title != null) {
+ sb.append(String.format("%s%n", title));
+ }
+
+ // Dump of active glyphs
+ sb.append(String.format("Active glyphs (%s) :%n",
+ getActiveGlyphs().size()));
+
+ for (Glyph glyph : getActiveGlyphs()) {
+ sb.append(String.format("%s%n", glyph));
+ }
+
+ // Dump of inactive glyphs
+ Collection inactives = new ArrayList<>(getAllGlyphs());
+ inactives.removeAll(getActiveGlyphs());
+ sb.append(String.format("%nInactive glyphs (%s) :%n", inactives.size()));
+
+ for (Glyph glyph : inactives) {
+ sb.append(String.format("%s%n", glyph));
+ }
+
+ return sb.toString();
+ }
+
+ //-----------------//
+ // getActiveGlyphs //
+ //-----------------//
+ @Override
+ public synchronized Collection getActiveGlyphs ()
+ {
+ if (activeGlyphs == null) {
+ activeGlyphs = Glyphs.sortedSet(activeMap.values());
+ activeGlyphs.addAll(virtualGlyphs);
+ }
+
+ return Collections.unmodifiableCollection(activeGlyphs);
+ }
+
+ //--------------//
+ // getAllGlyphs //
+ //--------------//
+ @Override
+ public Collection getAllGlyphs ()
+ {
+ return Collections.unmodifiableCollection(allGlyphs.values());
+ }
+
+ //----------//
+ // getGlyph //
+ //----------//
+ @Override
+ public Glyph getGlyph (Integer id)
+ {
+ return allGlyphs.get(id);
+ }
+
+ //-----------------//
+ // getGlyphService //
+ //-----------------//
+ @Override
+ public SelectionService getGlyphService ()
+ {
+ return glyphService;
+ }
+
+ //--------------//
+ // getHistogram //
+ //--------------//
+ @Override
+ public Histogram getHistogram (Orientation orientation,
+ Collection glyphs)
+ {
+ Histogram histo = new Histogram<>();
+
+ if (!glyphs.isEmpty()) {
+ Rectangle box = Glyphs.getBounds(glyphs);
+ Roi roi = new BasicRoi(box);
+ histo = roi.getSectionHistogram(
+ orientation,
+ Glyphs.sectionsOf(glyphs));
+ }
+
+ return histo;
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ @Override
+ public String getName ()
+ {
+ return name;
+ }
+
+ //-------------//
+ // getOriginal //
+ //-------------//
+ @Override
+ public Glyph getOriginal (Glyph glyph)
+ {
+ return getOriginal(glyph.getSignature());
+ }
+
+ //-------------//
+ // getOriginal //
+ //-------------//
+ @Override
+ public Glyph getOriginal (GlyphSignature signature)
+ {
+ // Find an old glyph registered with this signature
+ Glyph oldGlyph = originals.get(signature);
+
+ if (oldGlyph == null) {
+ return null;
+ }
+
+ // Check the old signature is still valid
+ if (oldGlyph.getSignature().compareTo(signature) == 0) {
+ return oldGlyph;
+ } else {
+ logger.debug("Obsolete signature for {}", oldGlyph);
+
+ return null;
+ }
+ }
+
+ //------------------//
+ // getSelectedGlyph //
+ //------------------//
+ @Override
+ public Glyph getSelectedGlyph ()
+ {
+ return (Glyph) getGlyphService().getSelection(GlyphEvent.class);
+ }
+
+ //---------------------//
+ // getSelectedGlyphSet //
+ //---------------------//
+ @SuppressWarnings("unchecked")
+ @Override
+ public Set getSelectedGlyphSet ()
+ {
+ return (Set) getGlyphService().getSelection(GlyphSetEvent.class);
+ }
+
+ //-------//
+ // isVip //
+ //-------//
+ @Override
+ public boolean isVip (Glyph glyph)
+ {
+ return params.vipGlyphs.contains(glyph.getId());
+ }
+
+ //--------------//
+ // lookupGlyphs //
+ //--------------//
+ @Override
+ public Set lookupGlyphs (Rectangle rect)
+ {
+ return Glyphs.lookupGlyphs(getActiveGlyphs(), rect);
+ }
+
+ //-------------------------//
+ // lookupIntersectedGlyphs //
+ //-------------------------//
+ @Override
+ public Set lookupIntersectedGlyphs (Rectangle rect)
+ {
+ return Glyphs.lookupIntersectedGlyphs(getActiveGlyphs(), rect);
+ }
+
+ //--------------------//
+ // lookupVirtualGlyph //
+ //--------------------//
+ @Override
+ public Glyph lookupVirtualGlyph (Point point)
+ {
+ for (Glyph virtual : virtualGlyphs) {
+ if (virtual.getBounds().contains(point)) {
+ return virtual;
+ }
+ }
+
+ return null;
+ }
+
+ //------------//
+ // mapSection //
+ //------------//
+ /**
+ * Map a section to a glyph, making the glyph active
+ *
+ * @param section the section to map
+ * @param glyph the assigned glyph
+ */
+ @Override
+ public synchronized void mapSection (Section section,
+ Glyph glyph)
+ {
+ if (glyph != null) {
+ activeMap.put(section, glyph);
+ } else {
+ activeMap.remove(section);
+ }
+
+ // Invalidate the collection of active glyphs
+ activeGlyphs = null;
+ }
+
+ //---------//
+ // onEvent //
+ //---------//
+ @Override
+ public void onEvent (UserEvent event)
+ {
+ try {
+ // Ignore RELEASING
+ if (event.movement == MouseMovement.RELEASING) {
+ return;
+ }
+
+ if (event instanceof LocationEvent) {
+ // Location => enclosed Glyph(s) or 1 virtual glyph
+ handleEvent((LocationEvent) event);
+ } else if (event instanceof GlyphEvent) {
+ // Glyph => glyph contour & GlyphSet update
+ handleEvent((GlyphEvent) event);
+ } else if (event instanceof GlyphSetEvent) {
+ // GlyphSet => Compound glyph
+ handleEvent((GlyphSetEvent) event);
+ } else if (event instanceof GlyphIdEvent) {
+ // Glyph Id => Glyph
+ handleEvent((GlyphIdEvent) event);
+ }
+ } catch (Throwable ex) {
+ logger.warn(getClass().getName() + " onEvent error", ex);
+ }
+ }
+
+ //---------------//
+ // registerGlyph //
+ //---------------//
+ @Override
+ public Glyph registerGlyph (Glyph glyph)
+ {
+ // First check this physical glyph does not already exist
+ Glyph original = getOriginal(glyph);
+
+ if (original != null) {
+ if (original != glyph) {
+ // Reuse the existing glyph
+ if (logger.isDebugEnabled()) {
+ logger.debug("new avatar of #{}{}{}",
+ original.getId(),
+ Sections.
+ toString(" members", glyph.getMembers()),
+ Sections.toString(" original", original.
+ getMembers()));
+ }
+
+ glyph = original;
+ glyph.setPartOf(null);
+ }
+ } else {
+ GlyphSignature newSig = glyph.getSignature();
+
+ if (glyph.isTransient()) {
+ // Register with a brand new Id
+ final int id = generateId();
+ glyph.setId(id);
+ glyph.setNest(this);
+ allGlyphs.put(id, glyph);
+
+ if (isVip(glyph)) {
+ glyph.setVip();
+ }
+ } else {
+ // This is a re-registration
+ GlyphSignature oldSig = glyph.getRegisteredSignature();
+
+ if ((oldSig != null) && !newSig.equals(oldSig)) {
+ Glyph oldGlyph = originals.remove(oldSig);
+
+ if (oldGlyph != null) {
+ logger.debug("Updating registration of {} oldGlyph:{}",
+ glyph.idString(), oldGlyph.getId());
+ }
+ }
+ }
+
+ originals.put(newSig, glyph);
+ glyph.setRegisteredSignature(newSig);
+
+ logger.debug("Registered {} as original {}",
+ glyph.idString(), glyph.getSignature());
+ }
+
+ // Special for virtual glyphs
+ if (glyph.isVirtual()) {
+ virtualGlyphs.add(glyph);
+ }
+
+ return glyph;
+ }
+
+ //--------------------//
+ // removeVirtualGlyph //
+ //--------------------//
+ @Override
+ public synchronized void removeVirtualGlyph (VirtualGlyph glyph)
+ {
+ originals.remove(glyph.getSignature(), glyph);
+ allGlyphs.remove(glyph.getId(), glyph);
+ virtualGlyphs.remove(glyph);
+ activeGlyphs = null;
+ }
+
+ //-------------//
+ // setServices //
+ //-------------//
+ @Override
+ public void setServices (SelectionService locationService)
+ {
+ this.locationService = locationService;
+
+ for (Class> eventClass : locEventsRead) {
+ locationService.subscribeStrongly(eventClass, this);
+ }
+
+ for (Class> eventClass : glyEventsRead) {
+ glyphService.subscribeStrongly(eventClass, this);
+ }
+ }
+
+ //-------------//
+ // setServices //
+ //-------------//
+ @Override
+ public void cutServices (SelectionService locationService)
+ {
+ for (Class> eventClass : locEventsRead) {
+ locationService.unsubscribe(eventClass, this);
+ }
+
+ for (Class> eventClass : glyEventsRead) {
+ glyphService.unsubscribe(eventClass, this);
+ }
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder("{Nest");
+
+ sb.append(" ").append(name);
+
+ // Active/All glyphs
+ if (!allGlyphs.isEmpty()) {
+ sb.append(" glyphs=").append(getActiveGlyphs().size()).append("/").
+ append(allGlyphs.size());
+ } else {
+ sb.append(" noglyphs");
+ }
+
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ //---------//
+ // publish //
+ //---------//
+ /**
+ * Publish on glyph service
+ *
+ * @param event the event to publish
+ */
+ protected void publish (NestEvent event)
+ {
+ glyphService.publish(event);
+ }
+
+ //---------//
+ // publish //
+ //---------//
+ /**
+ * Publish on location service
+ *
+ * @param event the event to publish
+ */
+ protected void publish (LocationEvent event)
+ {
+ locationService.publish(event);
+ }
+
+ //------------------//
+ // subscribersCount //
+ //------------------//
+ /**
+ * Convenient method to retrieve the number of subscribers on the glyph
+ * service for a specific class
+ *
+ * @param classe the specific classe
+ * @return the number of subscribers interested in the specific class
+ */
+ protected int subscribersCount (Class extends NestEvent> classe)
+ {
+ return glyphService.subscribersCount(classe);
+ }
+
+ //------------//
+ // generateId //
+ //------------//
+ private int generateId ()
+ {
+ return globalGlyphId.incrementAndGet();
+ }
+
+ //-------------//
+ // handleEvent //
+ //-------------//
+ /**
+ * Interest in sheet location => [active] glyph(s)
+ *
+ * @param locationEvent
+ */
+ private void handleEvent (LocationEvent locationEvent)
+ {
+ SelectionHint hint = locationEvent.hint;
+ MouseMovement movement = locationEvent.movement;
+ Rectangle rect = locationEvent.getData();
+
+ if (!hint.isLocation() && !hint.isContext()) {
+ return;
+ }
+
+ if (rect == null) {
+ return;
+ }
+
+ if ((rect.width > 0) && (rect.height > 0)) {
+ // This is a non-degenerated rectangle
+ // Look for set of enclosed active glyphs
+ Set glyphsFound = lookupGlyphs(rect);
+
+ // Publish Glyph
+ Glyph glyph = glyphsFound.isEmpty() ? null
+ : glyphsFound.iterator().next();
+ publish(new GlyphEvent(this, hint, movement, glyph));
+
+ // Publish GlyphSet
+ publish(new GlyphSetEvent(this, hint, movement, glyphsFound));
+ } else {
+ // This is just a point
+ Glyph glyph = lookupVirtualGlyph(
+ new Point(rect.getLocation()));
+
+ // Publish virtual Glyph, if any
+ if (glyph != null) {
+ publish(new GlyphEvent(this, hint, movement, glyph));
+ } else {
+ // No virtual glyph found, a standard glyph is found by:
+ // Pt -> (h/v)run -> (h/v)section -> glyph
+ // So there is nothing to do here, except nullifying glyph
+ publish(new GlyphEvent(this, hint, movement, null));
+
+ // And let proper lag publish non-null glyph later
+ // Since BasicNest is first subscriber on location (berk!)
+ }
+ }
+ }
+
+ //-------------//
+ // handleEvent //
+ //-------------//
+ /**
+ * Interest in Glyph => glyph contour & GlyphSet update
+ *
+ * @param glyphEvent
+ */
+ private void handleEvent (GlyphEvent glyphEvent)
+ {
+ SelectionHint hint = glyphEvent.hint;
+ MouseMovement movement = glyphEvent.movement;
+ Glyph glyph = glyphEvent.getData();
+
+ if ((hint == GLYPH_INIT) || (hint == GLYPH_MODIFIED)) {
+ // Display glyph contour
+ if (glyph != null) {
+ Rectangle box = glyph.getBounds();
+ publish(new LocationEvent(this, hint, movement, box));
+ }
+ }
+
+ // In glyph-selection mode, for non-transient glyphs
+ // (and only if we have interested subscribers)
+ if ((hint != GLYPH_TRANSIENT)
+ && !ViewParameters.getInstance().isSectionMode()
+ && (subscribersCount(GlyphSetEvent.class) > 0)) {
+ // Update glyph set
+ Set glyphs = getSelectedGlyphSet();
+
+ if (glyphs == null) {
+ glyphs = new LinkedHashSet<>();
+ }
+
+ if (hint == LOCATION_ADD) {
+ // Adding to (or Removing from) the set of glyphs
+ if (glyph != null) {
+ if (glyphs.contains(glyph)) {
+ glyphs.remove(glyph);
+ } else {
+ glyphs.add(glyph);
+ }
+ }
+ } else if (hint == CONTEXT_ADD) {
+ // Don't modify the set
+ } else {
+ // Overwriting the set of glyphs
+ if (glyph != null) {
+ // Make a one-glyph set
+ glyphs = Glyphs.sortedSet(glyph);
+ } else {
+ // Make an empty set
+ glyphs = Glyphs.sortedSet();
+ }
+ }
+
+ publish(new GlyphSetEvent(this, hint, movement, glyphs));
+ }
+ }
+
+ //-------------//
+ // handleEvent //
+ //-------------//
+ /**
+ * Interest in GlyphSet => Compound
+ *
+ * @param glyphSetEvent
+ */
+ private void handleEvent (GlyphSetEvent glyphSetEvent)
+ {
+ if (ViewParameters.getInstance().isSectionMode()) {
+ // Section mode
+ return;
+ }
+
+ // Glyph mode
+ MouseMovement movement = glyphSetEvent.movement;
+ Set glyphs = glyphSetEvent.getData();
+ Glyph compound = null;
+
+ if ((glyphs != null) && (glyphs.size() > 1)) {
+ try {
+ SystemInfo system = sheet.getSystemOf(glyphs);
+
+ if (system != null) {
+ compound = system.buildTransientCompound(glyphs);
+ publish(
+ new GlyphEvent(
+ this,
+ SelectionHint.GLYPH_TRANSIENT,
+ movement,
+ compound));
+ }
+ } catch (IllegalArgumentException ex) {
+ // All glyphs do not belong to the same system
+ // No compound is allowed and displayed
+ logger.warn("Selecting glyphs from different systems");
+ }
+ }
+ }
+
+ //-------------//
+ // handleEvent //
+ //-------------//
+ /**
+ * Interest in Glyph ID => glyph
+ *
+ * @param glyphIdEvent
+ */
+ private void handleEvent (GlyphIdEvent glyphIdEvent)
+ {
+ SelectionHint hint = glyphIdEvent.hint;
+ MouseMovement movement = glyphIdEvent.movement;
+ int id = glyphIdEvent.getData();
+
+ //TODO: Check the need for this:
+ // // Nullify Run entity
+ // publish(new RunEvent(this, hint, movement, null));
+ //
+ // // Nullify Section entity
+ // publish(new SectionEvent(this, hint, movement, null));
+
+ // Report Glyph entity (which may be null)
+ publish(new GlyphEvent(this, hint, movement, getGlyph(id)));
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-----------//
+ // Constants //
+ //-----------//
+ private static final class Constants
+ extends ConstantSet
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ Constant.String vipGlyphs = new Constant.String(
+ "",
+ "(Debug) Comma-separated list of VIP glyphs");
+
+ }
+
+ //------------//
+ // Parameters //
+ //------------//
+ /**
+ * Class {@code Parameters} gathers all constants related to nest
+ */
+ private static class Parameters
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ final List vipGlyphs; // List of IDs for VIP glyphs
+
+ //~ Constructors -------------------------------------------------------
+ public Parameters ()
+ {
+ vipGlyphs = VipUtil.decodeIds(constants.vipGlyphs.getValue());
+
+ if (logger.isDebugEnabled()) {
+ Main.dumping.dump(this);
+ }
+
+ if (!vipGlyphs.isEmpty()) {
+ logger.info("VIP glyphs: {}", vipGlyphs);
+ }
+ }
+ }
+}
diff --git a/src/main/omr/glyph/CompoundBuilder.java b/src/main/omr/glyph/CompoundBuilder.java
new file mode 100644
index 0000000..780c4c7
--- /dev/null
+++ b/src/main/omr/glyph/CompoundBuilder.java
@@ -0,0 +1,462 @@
+//----------------------------------------------------------------------------//
+// //
+// C o m p o u n d B u i l d e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.glyph.facets.Glyph;
+
+import omr.sheet.SystemInfo;
+
+import omr.util.Predicate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class {@code CompoundBuilder} defines a generic way to smartly
+ * build glyph compounds, and provides derived variants.
+ *
+ * @author Hervé Bitteur
+ */
+public class CompoundBuilder
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ CompoundBuilder.class);
+
+ //~ Instance fields --------------------------------------------------------
+ /** Dedicated system */
+ protected final SystemInfo system;
+
+ //~ Constructors -----------------------------------------------------------
+ //-----------------//
+ // CompoundBuilder //
+ //-----------------//
+ /**
+ * Creates a new CompoundBuilder object.
+ *
+ * @param system the containing system
+ */
+ public CompoundBuilder (SystemInfo system)
+ {
+ this.system = system;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //---------------//
+ // buildCompound //
+ //---------------//
+ /**
+ * Try to build a compound, starting from given seed and looking
+ * into the collection of suitable glyphs.
+ *
+ * If successful, this method assigns the proper shape to the compound,
+ * and inserts it in the system environment.
+ *
+ * @param seed the initial glyph around which the compound is built
+ * @param includeSeed true if seed must be included in compound
+ * @param suitables collection of potential glyphs
+ * @param adapter the specific behavior of the compound tests
+ * @return the compound built if successful, null otherwise
+ */
+ public Glyph buildCompound (Glyph seed,
+ boolean includeSeed,
+ Collection suitables,
+ CompoundAdapter adapter)
+ {
+ // Set seed (and reference box)
+ adapter.setSeed(seed);
+
+ // Retrieve good neighbors among the suitable glyphs
+ Set neighbors = new HashSet<>();
+
+ // Include the seed in the compound glyphs?
+ int minCount = 1;
+
+ if (includeSeed) {
+ neighbors.add(seed);
+ minCount++;
+ }
+
+ for (Glyph g : suitables) {
+ if (includeSeed || (g != seed)) {
+ if (adapter.isCandidateSuitable(g)
+ && adapter.isCandidateClose(g)) {
+ neighbors.add(g);
+ }
+ }
+ }
+
+ if (neighbors.size() >= minCount) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("neighbors={} seed={}",
+ Glyphs.toString(neighbors), seed);
+ }
+
+ Glyph compound = system.buildTransientCompound(neighbors);
+
+ if (adapter.isCompoundValid(compound)) {
+ // Assign and insert into system & nest environments
+ compound = system.addGlyph(compound);
+ compound.setEvaluation(adapter.getChosenEvaluation());
+
+ logger.debug("Compound #{} built as {}",
+ compound.getId(), compound.getShape());
+
+ return compound;
+ }
+ }
+
+ return null;
+ }
+
+ //---------------//
+ // buildCompound //
+ //---------------//
+ /**
+ * A basic building, which simply takes all the provided glyphs
+ * and build a persistent compound out of them.
+ *
+ * @param parts the glyphs to merge
+ * @return the compound built
+ */
+ public Glyph buildCompound (Collection parts)
+ {
+ if (parts.isEmpty()) {
+ return null;
+ }
+
+ List list = new ArrayList<>(parts);
+
+ return buildCompound(
+ list.get(0),
+ true,
+ list.subList(1, list.size()),
+ new NoAdapter(system));
+ }
+
+ //~ Inner Interfaces -------------------------------------------------------
+ //-----------------//
+ // CompoundAdapter //
+ //-----------------//
+ /**
+ * Interface {@code CompoundAdapter} provides the needed features
+ * for building compounds out of glyphs.
+ */
+ public static interface CompoundAdapter
+ {
+ //~ Methods ------------------------------------------------------------
+
+ /**
+ * Report the evaluation chosen for the compound.
+ *
+ * @return the evaluation (shape + grade) chosen
+ */
+ Evaluation getChosenEvaluation ();
+
+ /**
+ * Predicate to check whether a given candidate glyph is close
+ * enough to the reference box.
+ *
+ * @param glyph the glyph to check for proximity
+ * @return true if glyph is close enough
+ */
+ boolean isCandidateClose (Glyph glyph);
+
+ /**
+ * Predicate for a glyph to be a potential part of the building.
+ * (the location criteria is handled by {@link #isCandidateClose}).
+ *
+ * @param glyph the glyph to check
+ * @return true if the glyph is suitable for inclusion
+ */
+ boolean isCandidateSuitable (Glyph glyph);
+
+ /**
+ * Predicate to check the validity of the newly built compound.
+ * If valid, the chosenEvaluation is assigned accordingly.
+ *
+ * @param compound the resulting compound glyph to check
+ * @return true if the compound is found OK. The compound shape is not
+ * assigned by this method, but can be later retrieved through
+ * getChosenEvaluation() method.
+ */
+ boolean isCompoundValid (Glyph compound);
+
+ /**
+ * Define the seed glyph around which the compound will be built.
+ *
+ * @param seed the seed glyph
+ * @return the computed reference box
+ */
+ Rectangle setSeed (Glyph seed);
+
+ /**
+ * Should we filter the provided candidates?. (by calling
+ * {@link #isCandidateSuitable} and {@link #isCandidateClose}).
+ *
+ * @return true to apply filter
+ */
+ boolean shouldFilterCandidates ();
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-----------------//
+ // AbstractAdapter //
+ //-----------------//
+ /**
+ * Basic abstract class to implement the {@link CompoundAdapter}
+ * interface.
+ */
+ public abstract static class AbstractAdapter
+ implements CompoundAdapter
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** Dedicated system */
+ protected final SystemInfo system;
+
+ /** Maximum grade for a compound */
+ protected final double minGrade;
+
+ /** Originating seed */
+ protected Glyph seed;
+
+ /** Search box */
+ protected Rectangle box;
+
+ /** The result of compound evaluation */
+ protected Evaluation chosenEvaluation;
+
+ //~ Constructors -------------------------------------------------------
+ /**
+ * Construct an AbstractAdapter.
+ *
+ * @param system the containing system
+ * @param minGrade maximum acceptable grade for the compound shape
+ */
+ public AbstractAdapter (SystemInfo system,
+ double minGrade)
+ {
+ this.system = system;
+ this.minGrade = minGrade;
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public Evaluation getChosenEvaluation ()
+ {
+ // By default, use shape and grade from evaluator
+ return chosenEvaluation;
+ }
+
+ @Override
+ public boolean isCandidateClose (Glyph glyph)
+ {
+ // By default, use box intersection
+ return box.intersects(glyph.getBounds());
+ }
+
+ @Override
+ public Rectangle setSeed (Glyph seed)
+ {
+ this.seed = seed;
+ box = computeReferenceBox();
+
+ return box;
+ }
+
+ @Override
+ public boolean shouldFilterCandidates ()
+ {
+ // By default, filter candidates
+ return true;
+ }
+
+ /**
+ * Compute the reference box.
+ * This method is called when seed has just been set.
+ */
+ protected abstract Rectangle computeReferenceBox ();
+ }
+
+ //-----------//
+ // NoAdapter //
+ //-----------//
+ /**
+ * A passthrough fake adapter.
+ */
+ public static class NoAdapter
+ extends AbstractAdapter
+ {
+ //~ Constructors -------------------------------------------------------
+
+ public NoAdapter (SystemInfo system)
+ {
+ super(system, 0);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public Evaluation getChosenEvaluation ()
+ {
+ return new Evaluation(null, Evaluation.ALGORITHM);
+ }
+
+ @Override
+ public boolean isCandidateClose (Glyph glyph)
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isCandidateSuitable (Glyph glyph)
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isCompoundValid (Glyph compound)
+ {
+ return true;
+ }
+
+ @Override
+ public boolean shouldFilterCandidates ()
+ {
+ return false;
+ }
+
+ @Override
+ protected Rectangle computeReferenceBox ()
+ {
+ return null;
+ }
+ }
+
+ //---------------//
+ // TopRawAdapter //
+ //---------------//
+ /**
+ * This compound adapter tries to find some specific shapes among
+ * the top raw evaluations found for the compound.
+ */
+ public abstract static class TopRawAdapter
+ extends AbstractAdapter
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** Collection of desired shapes for a valid compound */
+ protected final EnumSet desiredShapes;
+
+ /** Specific predicate for desired shapes */
+ protected final Predicate predicate = new Predicate()
+ {
+ @Override
+ public boolean check (Shape shape)
+ {
+ return desiredShapes.contains(shape);
+ }
+ };
+
+ //~ Constructors -------------------------------------------------------
+ /**
+ * Create a TopRawAdapter instance.
+ *
+ * @param system the containing system
+ * @param minGrade maximum acceptable grade on compound shape
+ * @param desiredShapes the valid shapes for the compound
+ */
+ public TopRawAdapter (SystemInfo system,
+ double minGrade,
+ EnumSet desiredShapes)
+ {
+ super(system, minGrade);
+ this.desiredShapes = desiredShapes;
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public boolean isCompoundValid (Glyph compound)
+ {
+ // Check if a desired shape appears in the top raw evaluations
+ final Evaluation vote = GlyphNetwork.getInstance().rawVote(
+ compound,
+ minGrade,
+ predicate);
+
+ if (vote != null) {
+ chosenEvaluation = vote;
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ //-----------------//
+ // TopShapeAdapter //
+ //-----------------//
+ /**
+ * This compound adapter tries to find some specific shapes among
+ * the top evaluations found for the compound.
+ */
+ public abstract static class TopShapeAdapter
+ extends TopRawAdapter
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Create a TopShapeAdapter instance.
+ *
+ * @param system the containing system
+ * @param minGrade maximum acceptable grade on compound shape
+ * @param desiredShapes the valid shapes for the compound
+ */
+ public TopShapeAdapter (SystemInfo system,
+ double minGrade,
+ EnumSet desiredShapes)
+ {
+ super(system, minGrade, desiredShapes);
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public boolean isCompoundValid (Glyph compound)
+ {
+ // Check if a desired shape appears in the top evaluations
+ final Evaluation vote = GlyphNetwork.getInstance().vote(
+ compound,
+ system,
+ minGrade,
+ predicate);
+
+ if (vote != null) {
+ chosenEvaluation = vote;
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/main/omr/glyph/Evaluation.java b/src/main/omr/glyph/Evaluation.java
new file mode 100644
index 0000000..0d9d77b
--- /dev/null
+++ b/src/main/omr/glyph/Evaluation.java
@@ -0,0 +1,167 @@
+//----------------------------------------------------------------------------//
+// //
+// E v a l u a t i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.constant.Constant;
+
+/**
+ * Class {@code Evaluation} gathers a glyph shape, its grade and,
+ * if any, details about its failure (name of the check that failed).
+ *
+ * @author Hervé Bitteur
+ */
+public class Evaluation
+ implements Comparable
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Absolute confidence in shape manually assigned by the user. */
+ public static final double MANUAL = 300;
+
+ /** Confidence for in structurally assigned. */
+ public static final double ALGORITHM = 200;
+
+ //~ Instance fields --------------------------------------------------------
+ /** The evaluated shape. */
+ public Shape shape;
+
+ /**
+ * The evaluation grade (larger is better), generally provided by
+ * the neural network evaluator in the range 0 - 100.
+ */
+ public double grade;
+
+ /** The specific check that failed, if any. */
+ public Failure failure;
+
+ //~ Constructors -----------------------------------------------------------
+ //------------//
+ // Evaluation //
+ //------------//
+ /**
+ * Create an initialized evaluation instance.
+ *
+ * @param shape the shape this evaluation measures
+ * @param grade the measurement result (larger is better)
+ */
+ public Evaluation (Shape shape,
+ double grade)
+ {
+ this.shape = shape;
+ this.grade = grade;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------//
+ // compareTo //
+ //-----------//
+ /**
+ * To sort from best to worst.
+ *
+ * @param that the other evaluation instance
+ * @return -1,0 or +1
+ */
+ @Override
+ public int compareTo (Evaluation that)
+ {
+ if (this.grade > that.grade) {
+ return -1;
+ }
+
+ if (this.grade < that.grade) {
+ return +1;
+ }
+
+ return 0;
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(shape);
+ sb.append("(");
+
+ if (grade == MANUAL) {
+ sb.append("MANUAL");
+ } else if (grade == ALGORITHM) {
+ sb.append("ALGORITHM");
+ } else {
+ sb.append((float) grade);
+ }
+
+ if (failure != null) {
+ sb.append(" failure:")
+ .append(failure);
+ }
+
+ sb.append(")");
+
+ return sb.toString();
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //---------//
+ // Failure //
+ //---------//
+ /**
+ * A class to handle which specific check has failed in the
+ * evaluation.
+ */
+ public static class Failure
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** The name of the test that failed. */
+ public final String test;
+
+ //~ Constructors -------------------------------------------------------
+ public Failure (String test)
+ {
+ this.test = test;
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public String toString ()
+ {
+ return test;
+ }
+ }
+
+ //-------//
+ // Grade //
+ //-------//
+ /**
+ * A subclass of Constant.Double, meant to store a grade constant.
+ */
+ public static class Grade
+ extends Constant.Double
+ {
+ //~ Constructors -------------------------------------------------------
+
+ /**
+ * Specific constructor, where unit & name are assigned later.
+ *
+ * @param defaultValue the (double) default value
+ * @param description the semantic of the constant
+ */
+ public Grade (double defaultValue,
+ java.lang.String description)
+ {
+ super("Grade", defaultValue, description);
+ }
+ }
+}
diff --git a/src/main/omr/glyph/EvaluationEngine.java b/src/main/omr/glyph/EvaluationEngine.java
new file mode 100644
index 0000000..902ed2b
--- /dev/null
+++ b/src/main/omr/glyph/EvaluationEngine.java
@@ -0,0 +1,88 @@
+//----------------------------------------------------------------------------//
+// //
+// E v a l u a t i o n E n g i n e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.glyph.facets.Glyph;
+
+import omr.math.NeuralNetwork;
+
+import java.util.Collection;
+
+/**
+ * Interface {@code EvaluationEngine} describes the life-cycle of an
+ * evaluation engine.
+ *
+ * @author Hervé Bitteur
+ */
+public interface EvaluationEngine
+ extends ShapeEvaluator
+{
+ //~ Enumerations -----------------------------------------------------------
+
+ /** The various modes for starting the training of an evaluator. */
+ public static enum StartingMode
+ {
+ //~ Enumeration constant initializers ----------------------------------
+
+ /** Start with the current values. */
+ INCREMENTAL,
+ /** Start from
+ * scratch, with new initial values. */
+ SCRATCH;
+
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ /**
+ * Dump the internals of the engine.
+ */
+ void dump ();
+
+ /**
+ * Store the engine in XML format.
+ */
+ void marshal ();
+
+ /**
+ * Stop the on-going training.
+ */
+ void stop ();
+
+ /**
+ * Train the evaluator on the provided base of sample glyphs.
+ *
+ * @param base the collection of glyphs to train the evaluator
+ * @param monitor a monitoring interface
+ * @param mode specify the starting mode of the training session
+ */
+ void train (Collection base,
+ Monitor monitor,
+ StartingMode mode);
+
+ //~ Inner Interfaces -------------------------------------------------------
+ /**
+ * General monitoring interface to pass information about the
+ * training of an evaluator when processing a sample glyph.
+ */
+ public static interface Monitor
+ extends NeuralNetwork.Monitor
+ {
+ //~ Methods ------------------------------------------------------------
+
+ /**
+ * Entry called when a glyph is being processed.
+ *
+ * @param glyph the sample glyph being processed
+ */
+ void glyphProcessed (Glyph glyph);
+ }
+}
diff --git a/src/main/omr/glyph/GlyphInspector.java b/src/main/omr/glyph/GlyphInspector.java
new file mode 100644
index 0000000..ecb984d
--- /dev/null
+++ b/src/main/omr/glyph/GlyphInspector.java
@@ -0,0 +1,285 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h I n s p e c t o r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.constant.ConstantSet;
+
+import omr.glyph.facets.Glyph;
+
+import omr.sheet.Scale;
+import omr.sheet.SystemInfo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Class {@code GlyphInspector} is at a system level, dedicated to the
+ * inspection of retrieved glyphs, their recognition being usually
+ * based on features used by a shape evaluator.
+ *
+ * @author Hervé Bitteur
+ */
+public class GlyphInspector
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(GlyphInspector.class);
+
+ /** Shapes acceptable for a part candidate */
+ private static final EnumSet partShapes = EnumSet.of(
+ Shape.DOT_set,
+ Shape.NOISE,
+ Shape.CLUTTER,
+ Shape.STACCATISSIMO,
+ Shape.NOTEHEAD_VOID,
+ Shape.FLAG_1,
+ Shape.FLAG_1_UP);
+
+ //~ Instance fields --------------------------------------------------------
+ /** Dedicated system */
+ private final SystemInfo system;
+
+ //~ Constructors -----------------------------------------------------------
+ //----------------//
+ // GlyphInspector //
+ //----------------//
+ /**
+ * Create an GlyphInspector instance.
+ *
+ * @param system the dedicated system
+ */
+ public GlyphInspector (SystemInfo system)
+ {
+ this.system = system;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //----------------//
+ // evaluateGlyphs //
+ //----------------//
+ /**
+ * All unassigned symbol glyphs of a given system, for which we can
+ * get a positive vote from the evaluator, are assigned the voted
+ * shape.
+ *
+ * @param minGrade the lower limit on grade to accept an evaluation
+ */
+ public void evaluateGlyphs (double minGrade)
+ {
+ ShapeEvaluator evaluator = GlyphNetwork.getInstance();
+
+ for (Glyph glyph : system.getGlyphs()) {
+ if (glyph.getShape() == null) {
+ // Get vote
+ Evaluation vote = evaluator.vote(glyph, system, minGrade);
+
+ if (vote != null) {
+ glyph.setEvaluation(vote);
+ }
+ }
+ }
+ }
+
+ //---------------//
+ // inspectGlyphs //
+ //---------------//
+ /**
+ * Process the given system, by retrieving unassigned glyphs,
+ * evaluating and assigning them if OK, or trying compounds
+ * otherwise.
+ *
+ * @param minGrade the minimum acceptable grade for this processing
+ * @param wide flag for extra wide box
+ */
+ public void inspectGlyphs (double minGrade,
+ boolean wide)
+ {
+ logger.debug("S#{} inspectGlyphs start", system.getId());
+
+ // For Symbols & Leaves
+ system.retrieveGlyphs();
+ system.removeInactiveGlyphs();
+ evaluateGlyphs(minGrade);
+ system.removeInactiveGlyphs();
+
+ // For Compounds
+ retrieveCompounds(minGrade, wide);
+ system.removeInactiveGlyphs();
+ evaluateGlyphs(minGrade);
+ system.removeInactiveGlyphs();
+ }
+
+ //-------------------//
+ // retrieveCompounds //
+ //-------------------//
+ /**
+ * In the specified system, look for glyphs portions that should be
+ * considered as parts of compound glyphs.
+ *
+ * @param minGrade minimum acceptable grade
+ * @param wide flag for extra wide box
+ */
+ private void retrieveCompounds (double minGrade,
+ boolean wide)
+ {
+ // Use a copy to avoid concurrent modifications
+ List glyphs = new ArrayList<>(system.getGlyphs());
+
+ for (Glyph seed : glyphs) {
+ // Now process this seed, by looking at neighbors
+ BasicAdapter adapter = new BasicAdapter(system, minGrade, seed, wide);
+
+ if (adapter.isCandidateSuitable(seed)) {
+ system.buildCompound(seed, true, glyphs, adapter);
+ }
+ }
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-----------//
+ // Constants //
+ //-----------//
+ private static final class Constants
+ extends ConstantSet
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ Scale.Fraction boxMargin = new Scale.Fraction(
+ 0.25,
+ "Box margin to check intersection with compound");
+
+ Scale.Fraction boxWiden = new Scale.Fraction(
+ 0.5,
+ "Box special abscissa margin to check intersection with compound");
+
+ }
+
+ //--------------//
+ // BasicAdapter //
+ //--------------//
+ /**
+ * Class {@code BasicAdapter} is a CompoundAdapter meant to
+ * retrieve all compounds (in a system).
+ */
+ private static class BasicAdapter
+ extends CompoundBuilder.AbstractAdapter
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ private Glyph stem = null;
+
+ private int stemX;
+
+ private int stemToSeed;
+
+ private final boolean wide;
+
+ //~ Constructors -------------------------------------------------------
+ /**
+ * Construct a BasicAdapter around a given seed
+ *
+ * @param system the containing system
+ * @param minGrade minimum acceptable grade
+ */
+ public BasicAdapter (SystemInfo system,
+ double minGrade,
+ Glyph seed,
+ boolean wide)
+ {
+ super(system, minGrade);
+ this.wide = wide;
+
+ stem = seed.getFirstStem();
+
+ if (stem != null) {
+ // Remember this stem as a border
+ stemX = stem.getCentroid().x;
+ stemToSeed = seed.getCentroid().x - stemX;
+ }
+ }
+
+ //~ Methods ------------------------------------------------------------
+ @Override
+ public Rectangle computeReferenceBox ()
+ {
+ if (seed == null) {
+ throw new NullPointerException(
+ "Compound seed has not been set");
+ }
+
+ Rectangle newBox = seed.getBounds();
+
+ Scale scale = system.getScoreSystem().getScale();
+ int boxMargin = scale.toPixels(GlyphInspector.constants.boxMargin);
+ int boxWiden = scale.toPixels(GlyphInspector.constants.boxWiden);
+
+ if (wide) {
+ newBox.grow(boxWiden, boxMargin);
+ } else {
+ newBox.grow(boxMargin, boxMargin);
+ }
+
+ return newBox;
+ }
+
+ @Override
+ public boolean isCandidateSuitable (Glyph glyph)
+ {
+ Shape shape = glyph.getShape();
+
+ if (!glyph.isActive() || (shape == Shape.LEDGER)) {
+ return false;
+ }
+
+ if (glyph.isKnown()
+ && (glyph.isManualShape()
+ || (!partShapes.contains(shape)
+ && (glyph.getGrade() > Grades.compoundPartMaxGrade)))) {
+ return false;
+ }
+
+ // Stay on same side of the stem if any
+ if ((stem != null)) {
+ return ((glyph.getCentroid().x - stemX) * stemToSeed) > 0;
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public boolean isCompoundValid (Glyph compound)
+ {
+ Evaluation eval = GlyphNetwork.getInstance().vote(compound, system,
+ minGrade);
+
+ if ((eval != null)
+ && eval.shape.isWellKnown()
+ && (eval.shape != Shape.CLUTTER)
+ && (!seed.isKnown() || (eval.grade > seed.getGrade()))) {
+ chosenEvaluation = eval;
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/main/omr/glyph/GlyphNetwork.java b/src/main/omr/glyph/GlyphNetwork.java
new file mode 100644
index 0000000..08d24ed
--- /dev/null
+++ b/src/main/omr/glyph/GlyphNetwork.java
@@ -0,0 +1,551 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h N e t w o r k //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.constant.Constant;
+import omr.constant.ConstantSet;
+
+import omr.glyph.facets.Glyph;
+
+import omr.math.NeuralNetwork;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+
+import javax.xml.bind.JAXBException;
+
+/**
+ * Class {@code GlyphNetwork} encapsulates a neural network customized
+ * for glyph recognition.
+ * It wraps the generic {@link NeuralNetwork} with application
+ * information, for training, storing, loading and using the neural network.
+ *
+ * The application neural network data is loaded as follows:
+ * It first tries to find a file named 'eval/neural-network.xml' in the
+ * application user area.
+ * If any, this file contains a custom definition of the network, typically
+ * after a user training.
+ *
+ * If not found, it falls back reading the default definition from the
+ * application resource, reading the 'res/neural-network.xml' file in the
+ * application program area.
+ *
+ * After a user training of the neural network, the data is stored as
+ * the custom definition in the user local file 'eval/neural-network.xml',
+ * which will be picked up first when the application is run again.
+ *
+ * @author Hervé Bitteur
+ */
+public class GlyphNetwork
+ extends AbstractEvaluationEngine
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(GlyphNetwork.class);
+
+ /** The singleton. */
+ private static volatile GlyphNetwork INSTANCE;
+
+ /** Neural network file name. */
+ private static final String FILE_NAME = "neural-network.xml";
+
+ //~ Instance fields --------------------------------------------------------
+ //
+ /** The underlying neural network. */
+ private NeuralNetwork engine;
+
+ //~ Constructors -----------------------------------------------------------
+ //
+ //--------------//
+ // GlyphNetwork //
+ //--------------//
+ /**
+ * Private constructor, to create a glyph neural network.
+ */
+ private GlyphNetwork ()
+ {
+ // Unmarshal from user or default data, if compatible
+ engine = (NeuralNetwork) unmarshal();
+
+ if (engine == null) {
+ // Get a brand new one (not trained)
+ logger.info("Creating a brand new {}", getName());
+ engine = createNetwork();
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //
+ //-------------//
+ // getInstance //
+ //-------------//
+ /**
+ * Report the single instance of GlyphNetwork in the application.
+ *
+ * @return the instance
+ */
+ public static GlyphNetwork getInstance ()
+ {
+ if (INSTANCE == null) {
+ synchronized (GlyphNetwork.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new GlyphNetwork();
+ }
+ }
+ }
+
+ return INSTANCE;
+ }
+
+ //--------------//
+ // isCompatible //
+ //--------------//
+ @Override
+ protected final boolean isCompatible (Object obj)
+ {
+ if (obj instanceof NeuralNetwork) {
+ NeuralNetwork anEngine = (NeuralNetwork) obj;
+
+ if (!Arrays.equals(anEngine.getInputLabels(),
+ ShapeDescription.getParameterLabels())) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Engine inputs: {}",
+ Arrays.toString(anEngine.getInputLabels()));
+ logger.debug("Shape inputs: {}",
+ Arrays.toString(ShapeDescription.getParameterLabels()));
+ }
+ return false;
+ }
+ if (!Arrays.equals(anEngine.getOutputLabels(),
+ ShapeSet.getPhysicalShapeNames())) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Engine outputs: {}",
+ Arrays.toString(anEngine.getOutputLabels()));
+ logger.debug("Physical shapes: {}",
+ Arrays.toString(ShapeSet.getPhysicalShapeNames()));
+ }
+ return false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ //------//
+ // dump //
+ //------//
+ /**
+ * Dump the internals of the neural network to the standard output.
+ */
+ @Override
+ public void dump ()
+ {
+ engine.dump();
+ }
+
+ //--------------//
+ // getAmplitude //
+ //--------------//
+ /**
+ * Selector for the amplitude value (used in initial random values).
+ *
+ * @return the amplitude value
+ */
+ public double getAmplitude ()
+ {
+ return constants.amplitude.getValue();
+ }
+
+ //-----------------//
+ // getLearningRate //
+ //-----------------//
+ /**
+ * Selector of the current value for network learning rate.
+ *
+ * @return the current learning rate
+ */
+ public double getLearningRate ()
+ {
+ return constants.learningRate.getValue();
+ }
+
+ //---------------//
+ // getListEpochs //
+ //---------------//
+ /**
+ * Selector on the maximum numner of training iterations.
+ *
+ * @return the upper limit on iteration counter
+ */
+ public int getListEpochs ()
+ {
+ return constants.listEpochs.getValue();
+ }
+
+ //-------------//
+ // getMaxError //
+ //-------------//
+ /**
+ * Report the error threshold to potentially stop the training
+ * process.
+ *
+ * @return the threshold currently in use
+ */
+ public double getMaxError ()
+ {
+ return constants.maxError.getValue();
+ }
+
+ //-------------//
+ // getMomentum //
+ //-------------//
+ /**
+ * Report the momentum training value currently in use.
+ *
+ * @return the momentum in use
+ */
+ public double getMomentum ()
+ {
+ return constants.momentum.getValue();
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ /**
+ * Report a name for this network.
+ *
+ * @return a simple name
+ */
+ @Override
+ public final String getName ()
+ {
+ return "Neural Network";
+ }
+
+ //------------//
+ // getNetwork //
+ //------------//
+ /**
+ * Selector to the encapsulated Neural Network.
+ *
+ * @return the neural network
+ */
+ public NeuralNetwork getNetwork ()
+ {
+ return engine;
+ }
+
+ //--------------//
+ // setAmplitude //
+ //--------------//
+ /**
+ * Set the amplitude value for initial random values (UNUSED).
+ *
+ * @param amplitude
+ */
+ public void setAmplitude (double amplitude)
+ {
+ constants.amplitude.setValue(amplitude);
+ }
+
+ //-----------------//
+ // setLearningRate //
+ //-----------------//
+ /**
+ * Dynamically modify the learning rate of the neural network for
+ * its training task.
+ *
+ * @param learningRate new learning rate to use
+ */
+ public void setLearningRate (double learningRate)
+ {
+ constants.learningRate.setValue(learningRate);
+ engine.setLearningRate(learningRate);
+ }
+
+ //---------------//
+ // setListEpochs //
+ //---------------//
+ /**
+ * Modify the upper limit on the number of epochs (training
+ * iterations) for the training process.
+ *
+ * @param listEpochs new value for iteration limit
+ */
+ public void setListEpochs (int listEpochs)
+ {
+ constants.listEpochs.setValue(listEpochs);
+ engine.setEpochs(listEpochs);
+ }
+
+ //-------------//
+ // setMaxError //
+ //-------------//
+ /**
+ * Modify the error threshold to potentially stop the training
+ * process.
+ *
+ * @param maxError the new threshold value to use
+ */
+ public void setMaxError (double maxError)
+ {
+ constants.maxError.setValue(maxError);
+ engine.setMaxError(maxError);
+ }
+
+ //-------------//
+ // setMomentum //
+ //-------------//
+ /**
+ * Modify the value for momentum used from learning epoch to the
+ * other.
+ *
+ * @param momentum the new momentum value to be used
+ */
+ public void setMomentum (double momentum)
+ {
+ constants.momentum.setValue(momentum);
+ engine.setMomentum(momentum);
+ }
+
+ //------//
+ // stop //
+ //------//
+ /**
+ * Forward the "Stop" order to the network being trained.
+ */
+ @Override
+ public void stop ()
+ {
+ engine.stop();
+ }
+
+ //-------//
+ // train //
+ //-------//
+ /**
+ * Train the network using the provided collection of glyphs.
+ *
+ * @param glyphs the provided collection of glyphs
+ * @param monitor the monitoring entity if any
+ * @param mode the starting mode of the trainer (scratch or incremental)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void train (Collection glyphs,
+ Monitor monitor,
+ StartingMode mode)
+ {
+ if (glyphs.isEmpty()) {
+ logger.warn("No glyph to retrain Neural Network evaluator");
+
+ return;
+ }
+
+ int quorum = constants.quorum.getValue();
+
+ // Determine cardinality for each shape
+ EnumMap> shapeGlyphs = new EnumMap<>(Shape.class);
+
+ for (Glyph glyph : glyphs) {
+ Shape shape = glyph.getShape();
+ List list = shapeGlyphs.get(shape);
+ if (list == null) {
+ list = new ArrayList<>();
+ shapeGlyphs.put(shape, list);
+ }
+ list.add(glyph);
+ }
+
+ List newGlyphs = new ArrayList<>();
+
+ for (List list : shapeGlyphs.values()) {
+ int card = 0;
+ boolean first = true;
+
+ if (!list.isEmpty()) {
+ while (card < quorum) {
+ for (int i = 0; i < list.size(); i++) {
+ newGlyphs.add(list.get(i));
+ card++;
+
+ if (!first && (card >= quorum)) {
+ break;
+ }
+ }
+
+ first = false;
+ }
+ }
+ }
+
+ // Shuffle the final collection of glyphs
+ Collections.shuffle(newGlyphs);
+
+ // Build the collection of patterns from the glyph data
+ double[][] inputs = new double[newGlyphs.size()][];
+ double[][] desiredOutputs = new double[newGlyphs.size()][];
+
+ int ig = 0;
+
+ for (Glyph glyph : newGlyphs) {
+ double[] ins = ShapeDescription.features(glyph);
+ inputs[ig] = ins;
+
+ double[] des = new double[shapeCount];
+ Arrays.fill(des, 0);
+
+ des[glyph.getShape().getPhysicalShape().ordinal()] = 1;
+ desiredOutputs[ig] = des;
+
+ ig++;
+ }
+
+ // Starting options
+ if (mode == StartingMode.SCRATCH) {
+ engine = createNetwork();
+ }
+
+ // Train on the patterns
+ engine.train(inputs, desiredOutputs, monitor);
+ }
+
+ //-------------//
+ // getFileName //
+ //-------------//
+ @Override
+ protected String getFileName ()
+ {
+ return FILE_NAME;
+ }
+
+ //-------------------//
+ // getRawEvaluations //
+ //-------------------//
+ @Override
+ protected Evaluation[] getRawEvaluations (Glyph glyph)
+ {
+ // If too small, it's just NOISE
+ if (!isBigEnough(glyph)) {
+ return noiseEvaluations;
+ } else {
+ double[] ins = ShapeDescription.features(glyph);
+ double[] outs = new double[shapeCount];
+ Evaluation[] evals = new Evaluation[shapeCount];
+ Shape[] values = Shape.values();
+
+ engine.run(ins, null, outs);
+
+ for (int s = 0; s < shapeCount; s++) {
+ Shape shape = values[s];
+ // Use a grade in 0 .. 100 range
+ evals[s] = new Evaluation(shape, 100 * outs[s]);
+ }
+
+ // Order the evals from best to worst
+ Arrays.sort(evals);
+
+ return evals;
+ }
+ }
+
+ //---------//
+ // marshal //
+ //---------//
+ @Override
+ protected void marshal (OutputStream os)
+ throws FileNotFoundException, IOException, JAXBException
+ {
+ engine.marshal(os);
+ }
+
+ //-----------//
+ // unmarshal //
+ //-----------//
+ @Override
+ protected NeuralNetwork unmarshal (InputStream is)
+ throws JAXBException, IOException
+ {
+ return NeuralNetwork.unmarshal(is);
+ }
+
+ //---------------//
+ // createNetwork //
+ //---------------//
+ private NeuralNetwork createNetwork ()
+ {
+ // Note : We allocate a hidden layer with as many cells as the output
+ // layer
+ NeuralNetwork nn = new NeuralNetwork(
+ ShapeDescription.length(),
+ shapeCount,
+ shapeCount,
+ getAmplitude(),
+ ShapeDescription.getParameterLabels(), // Input labels
+ ShapeSet.getPhysicalShapeNames(), // Output labels
+ getLearningRate(),
+ getMomentum(),
+ getMaxError(),
+ getListEpochs());
+
+ return nn;
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ private static final class Constants
+ extends ConstantSet
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ Constant.Ratio amplitude = new Constant.Ratio(
+ 0.5,
+ "Initial weight amplitude");
+
+ Constant.Ratio learningRate = new Constant.Ratio(
+ 0.2,
+ "Learning Rate");
+
+ Constant.Integer listEpochs = new Constant.Integer(
+ "Epochs",
+ 4000,
+ "Number of epochs for training on list of glyphs");
+
+ Constant.Integer quorum = new Constant.Integer(
+ "Glyphs",
+ 10,
+ "Minimum number of glyphs for each shape");
+
+ Evaluation.Grade maxError = new Evaluation.Grade(
+ 1E-3,
+ "Threshold to stop training");
+
+ Constant.Ratio momentum = new Constant.Ratio(0.2, "Training momentum");
+
+ }
+}
diff --git a/src/main/omr/glyph/GlyphRegression.java b/src/main/omr/glyph/GlyphRegression.java
new file mode 100644
index 0000000..d1b24fd
--- /dev/null
+++ b/src/main/omr/glyph/GlyphRegression.java
@@ -0,0 +1,875 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h R e g r e s s i o n //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.Main;
+
+import omr.constant.Constant;
+import omr.constant.ConstantSet;
+
+import omr.glyph.facets.Glyph;
+
+import omr.math.LinearEvaluator;
+import omr.math.LinearEvaluator.Sample;
+
+import org.jdesktop.application.Application.ExitListener;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EventObject;
+import java.util.List;
+
+import javax.xml.bind.JAXBException;
+
+/**
+ * Class {@code GlyphRegression} is a glyph evaluator that encapsulates
+ * a {@link LinearEvaluator} working on glyph parameters.
+ *
+ * @author Hervé Bitteur
+ */
+public class GlyphRegression
+ extends AbstractEvaluationEngine
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ GlyphRegression.class);
+
+ /** LinearEvaluator backup file name */
+ private static final String BACKUP_FILE_NAME = "linear-evaluator.xml";
+
+ /** The singleton */
+ private static volatile GlyphRegression INSTANCE;
+
+ //~ Instance fields --------------------------------------------------------
+ /** The encapsulated linear evaluator */
+ private LinearEvaluator engine;
+
+ /** The constraints (minimum, maximum) per shape & per parameter */
+ private EnumMap constraintMap = new EnumMap<>(
+ Shape.class);
+
+ //~ Constructors -----------------------------------------------------------
+ //-----------------//
+ // GlyphRegression //
+ //-----------------//
+ /**
+ * Private constructor
+ */
+ private GlyphRegression ()
+ {
+ // Unmarshal from backup data
+ engine = (LinearEvaluator) unmarshal();
+
+ if (engine == null) {
+ // Get a brand new one (not trained)
+ logger.info("Creating a brand new {}", getName());
+ engine = new LinearEvaluator(ShapeDescription.getParameterLabels());
+ } else {
+ defineConstraints();
+
+ // debug
+ // for (Shape shape : ShapeSet.allPhysicalShapes) {
+ // dumpOneShapeConstraints(shape);
+ // }
+ }
+
+ // Listen to application exit
+ if (Main.getGui() != null) {
+ Main.getGui().addExitListener(
+ new ExitListener()
+ {
+ @Override
+ public boolean canExit (EventObject eo)
+ {
+ return true;
+ }
+
+ @Override
+ public void willExit (EventObject eo)
+ {
+ if (engine.isDataModified()) {
+ marshal();
+ }
+ }
+ });
+ }
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ // //--------------------//
+ // // constraintsMatched //
+ // //--------------------//
+ // /**
+ // * Check that all the (non-disabled) constraints matched between a
+ // * given glyph and a shape
+ // * @param params the glyph features
+ // * @param eval the evaluation context to update
+ // * @return true if matched, false otherwise
+ // */
+ // public boolean constraintsMatched (double[] params,
+ // Evaluation eval)
+ // {
+ // String failed = firstMisMatched(params, eval.shape);
+ //
+ // if (failed != null) {
+ // eval.failure = new Evaluation.Failure(failed);
+ //
+ // return false;
+ // } else {
+ // return true;
+ // }
+ // }
+ //--------------//
+ // isCompatible //
+ //--------------//
+ @Override
+ protected final boolean isCompatible (Object obj)
+ {
+ if (obj instanceof LinearEvaluator) {
+ LinearEvaluator anEngine = (LinearEvaluator) obj;
+
+ // Check parameters names, they must be identical
+ if (!Arrays.equals(
+ anEngine.getParameterNames(),
+ ShapeDescription.getParameterLabels())) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Engine parameters: {}",
+ Arrays.toString(anEngine.getParameterNames()));
+ logger.debug("Shape parameters: {}",
+ Arrays.toString(ShapeDescription.getParameterLabels()));
+ }
+ return false;
+ }
+
+ // Check categories names. Order is not relevant
+ // Engine categories must be a subset of physical shapes
+ String[] categories = anEngine.getCategoryNames();
+
+ String[] shapes = ShapeSet.getPhysicalShapeNames();
+ String[] sortedShapes = Arrays.copyOf(shapes, shapes.length);
+
+ List extraNames = new ArrayList<>(Arrays.asList(categories));
+ extraNames.removeAll(Arrays.asList(sortedShapes));
+
+ if (!extraNames.isEmpty()) {
+ if (logger.isDebugEnabled()) {
+ Arrays.sort(categories);
+ logger.debug("Engine categories: {}",
+ Arrays.toString(categories));
+ Arrays.sort(sortedShapes);
+ logger.debug("Physical shapes: {}",
+ Arrays.toString(sortedShapes));
+ logger.debug("Extra names found in {}: {}",
+ getName(), extraNames);
+ }
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ //------//
+ // dump //
+ //------//
+ @Override
+ public void dump ()
+ {
+ engine.dump();
+ }
+
+ //--------------//
+ // dumpDistance //
+ //--------------//
+ /**
+ * Print out the "distance" information between a given glyph and a
+ * shape. It's a sort of debug information.
+ *
+ * @param glyph the glyph at hand
+ * @param shape the shape to measure distance from
+ */
+ public void dumpDistance (Glyph glyph,
+ Shape shape)
+ {
+ // shapeDescs[shape.ordinal()].dumpDistance(glyph);
+ }
+
+ //-------------------------//
+ // dumpOneShapeConstraints //
+ //-------------------------//
+ public void dumpOneShapeConstraints (Shape shape)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(String.format("Constraints for %s: ", shape));
+
+ String[] labels = ShapeDescription.getParameterLabels();
+ Range[] ranges = constraintMap.get(shape);
+
+ if (ranges == null) {
+ sb.append(String.format("none%n"));
+ } else {
+ sb.append(String.format("%n"));
+
+ for (int p = 0; p < ShapeDescription.length(); p++) {
+ StringBuilder sbp = new StringBuilder();
+ Range range = ranges[p];
+
+ if (range != null) {
+ Double min = range.min;
+
+ if (min != null) {
+ sbp.append(" min=").append(min);
+ }
+
+ Double max = range.max;
+
+ if (max != null) {
+ sbp.append(" max=").append(max);
+ }
+ }
+
+ if (sbp.length() > 0) {
+ sb.append(String.format(" %s:%s%n", labels[p], sbp));
+ }
+ }
+ }
+
+ logger.info(sb.toString());
+ }
+
+ //-----------//
+ // getEngine //
+ //-----------//
+ /**
+ * @return the engine
+ */
+ public LinearEvaluator getEngine ()
+ {
+ return engine;
+ }
+
+ //-------------//
+ // getInstance //
+ //-------------//
+ /**
+ * Provide access to the single instance of GlyphRegression for the
+ * application
+ *
+ * @return the GlyphRegression instance
+ */
+ public static GlyphRegression getInstance ()
+ {
+ if (INSTANCE == null) {
+ synchronized (GlyphRegression.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new GlyphRegression();
+ }
+ }
+ }
+
+ return INSTANCE;
+ }
+
+ //------------//
+ // getMaximum //
+ //------------//
+ /**
+ * Get the constraint test on maximum for a parameter of the
+ * provided shape.
+ *
+ * @param paramIndex the impacted parameter
+ * @param shape the targeted shape
+ * @return the current maximum value (null if test is disabled)
+ */
+ public Double getMaximum (int paramIndex,
+ Shape shape)
+ {
+ Range[] ranges = constraintMap.get(shape);
+
+ if (ranges == null) {
+ return null;
+ }
+
+ Range range = ranges[paramIndex];
+
+ return (range != null) ? range.max : null;
+ }
+
+ //------------//
+ // getMinimum //
+ //------------//
+ /**
+ * Get the constraint test on minimum for a parameter of the
+ * provided shape.
+ *
+ * @param paramIndex the impacted parameter
+ * @param shape the targeted shape
+ * @return the current minimum value (null if test is disabled)
+ */
+ public Double getMinimum (int paramIndex,
+ Shape shape)
+ {
+ Range[] ranges = constraintMap.get(shape);
+
+ if (ranges == null) {
+ return null;
+ }
+
+ Range range = ranges[paramIndex];
+
+ return (range != null) ? range.min : null;
+ }
+
+ //---------//
+ // getName //
+ //---------//
+ @Override
+ public final String getName ()
+ {
+ return "Linear Evaluator";
+ }
+
+ //---------------//
+ // includeSample //
+ //---------------//
+ /**
+ * Take into account the observed parameters for the provided shape,
+ * and relax the related constraints if needed.
+ *
+ * @param params the observed input parameters
+ * @param shape the provided shape
+ * @return true if constraints have been extended
+ */
+ public boolean includeSample (double[] params,
+ Shape shape)
+ {
+ // Include this observation
+ boolean extended = engine.includeSample(params, shape.toString());
+
+ if (extended) {
+ // Update extended constraints for the shape
+ defineOneShapeConstraints(shape);
+ }
+
+ return extended;
+ }
+
+ //-----------------//
+ // measureDistance //
+ //-----------------//
+ /**
+ * Measure the "distance" information between a given glyph and a
+ * shape.
+ *
+ * @param glyph the glyph at hand
+ * @param shape the shape to measure distance from
+ * @return the measured distance
+ */
+ public double measureDistance (Glyph glyph,
+ Shape shape)
+ {
+ return engine.categoryDistance(
+ ShapeDescription.features(glyph),
+ shape.toString());
+ }
+
+ //-----------------//
+ // measureDistance //
+ //-----------------//
+ /**
+ * Measure the "distance" information between a given glyph and a
+ * shape.
+ *
+ * @param ins the input parameters
+ * @param shape the shape to measure distance from
+ * @return the measured distance
+ */
+ public double measureDistance (double[] ins,
+ Shape shape)
+ {
+ return engine.categoryDistance(ins, shape.toString());
+ }
+
+ //-----------------//
+ // measureDistance //
+ //-----------------//
+ /**
+ * Measure the "distance" information between two glyphs.
+ *
+ * @param one the first glyph
+ * @param two the second glyph
+ * @return the measured distance
+ */
+ public double measureDistance (Glyph one,
+ Glyph two)
+ {
+ return measureDistance(one, ShapeDescription.features(two));
+ }
+
+ //-----------------//
+ // measureDistance //
+ //-----------------//
+ /**
+ * Measure the "distance" information between a glyph and an array
+ * of parameters (generally fed from another glyph).
+ *
+ * @param glyph the given glyph
+ * @param ins the array (size = paramCount) of parameters
+ * @return the measured distance
+ */
+ public double measureDistance (Glyph glyph,
+ double[] ins)
+ {
+ return engine.patternDistance(ShapeDescription.features(glyph), ins);
+ }
+
+ //------------//
+ // setMaximum //
+ //------------//
+ /**
+ * Set the constraint test on maximum for a parameter of the
+ * provided shape.
+ *
+ * @param paramIndex the impacted parameter
+ * @param shape the targeted shape
+ * @param val the new maximum value (null for disabling the test)
+ */
+ public void setMaximum (int paramIndex,
+ Shape shape,
+ Double val)
+ {
+ doGetRange(paramIndex, shape).max = val;
+ }
+
+ //------------//
+ // setMinimum //
+ //------------//
+ /**
+ * Set the constraint test on minimum for a parameter of the
+ * provided shape.
+ *
+ * @param paramIndex the impacted parameter
+ * @param shape the targeted shape
+ * @param val the new minimum value (null for disabling the test)
+ */
+ public void setMinimum (int paramIndex,
+ Shape shape,
+ Double val)
+ {
+ doGetRange(paramIndex, shape).min = val;
+ }
+
+ //-------//
+ // train //
+ //-------//
+ /**
+ * Launch the training of the evaluator.
+ *
+ * @param base the collection of glyphs used for training
+ * @param monitor a monitoring entity
+ * @param mode incremental or scratch mode
+ */
+ @Override
+ public void train (Collection base,
+ Monitor monitor,
+ StartingMode mode)
+ {
+ if (base.isEmpty()) {
+ logger.warn("No glyph to retrain Regression Evaluator");
+
+ return;
+ }
+
+ // Prepare the collection of samples
+ Collection samples = new ArrayList<>();
+
+ for (Glyph glyph : base) {
+ try {
+ Shape shape = glyph.getShape().getPhysicalShape();
+ Sample sample = new Sample(
+ shape.toString(),
+ ShapeDescription.features(glyph));
+ samples.add(sample);
+ } catch (Exception ex) {
+ logger.warn(
+ "Weird glyph shape: " + glyph.getShape() + " file="
+ + GlyphRepository.getInstance().getGlyphName(glyph),
+ ex);
+ }
+ }
+
+ // Do the training
+ engine.train(samples);
+
+ // Save to disk
+ marshal();
+ }
+
+ //-------------//
+ // getFileName //
+ //-------------//
+ @Override
+ protected String getFileName ()
+ {
+ return BACKUP_FILE_NAME;
+ }
+
+ //-------------------//
+ // getRawEvaluations //
+ //-------------------//
+ @Override
+ protected Evaluation[] getRawEvaluations (Glyph glyph)
+ {
+ // If too small, it's just NOISE
+ if (!isBigEnough(glyph)) {
+ return noiseEvaluations;
+ } else {
+ double[] ins = ShapeDescription.features(glyph);
+ Evaluation[] evals = new Evaluation[shapeCount];
+ Shape[] values = Shape.values();
+
+ for (int s = 0; s < shapeCount; s++) {
+ Shape shape = values[s];
+ evals[s] = new Evaluation(
+ shape,
+ 1d / measureDistance(ins, shape));
+ }
+
+ // Order the evals from best to worst
+ Arrays.sort(evals);
+
+ return evals;
+ }
+ }
+
+ //---------//
+ // marshal //
+ //---------//
+ @Override
+ protected void marshal (OutputStream os)
+ throws FileNotFoundException, IOException, JAXBException
+ {
+ engine.marshal(os);
+ }
+
+ //-----------//
+ // unmarshal //
+ //-----------//
+ @Override
+ protected LinearEvaluator unmarshal (InputStream is)
+ throws JAXBException
+ {
+ return LinearEvaluator.unmarshal(is);
+ }
+
+ //-------------------//
+ // defineConstraints //
+ //-------------------//
+ /**
+ * Here we customize the linear evaluator to our specific needs,
+ * by removing some constraint checks and relaxing others.
+ */
+ private void defineConstraints ()
+ {
+ for (Shape shape : ShapeSet.allPhysicalShapes) {
+ defineOneShapeConstraints(shape);
+ }
+ }
+
+ //---------------------------//
+ // defineOneShapeConstraints //
+ //---------------------------//
+ /**
+ * Here we customize the constraints to our specific needs, by
+ * removing some constraint checks and relaxing others.
+ *
+ * @param shape the shape at hand
+ */
+ private void defineOneShapeConstraints (Shape shape)
+ {
+ // // First, use LinearEvaluator observed constraints
+ // for (int p = 0; p < ShapeDescription.length(); p++) {
+ // setMinimum(p, shape, engine.getMinimum(p, shape.name()));
+ // setMaximum(p, shape, engine.getMaximum(p, shape.name()));
+ // }
+ //
+ // // Second, relax some constraints
+ // // Add some margin around constraints
+ // double minFactor = constants.factorForMinima.getValue();
+ // double maxFactor = constants.factorForMaxima.getValue();
+ //
+ // for (String label : Arrays.asList("weight", "width", "height")) {
+ // int p = ShapeDescription.getParameterIndex(label);
+ // Double val = getMinimum(p, shape);
+ //
+ // if (val != null) {
+ // if (val > 0) {
+ // setMinimum(p, shape, val * minFactor);
+ // } else {
+ // setMinimum(p, shape, val * maxFactor);
+ // }
+ // }
+ //
+ // val = getMaximum(p, shape);
+ //
+ // if (val != null) {
+ // if (val > 0) {
+ // setMaximum(p, shape, val * maxFactor);
+ // } else {
+ // setMaximum(p, shape, val * minFactor);
+ // }
+ // }
+ // }
+ //
+ // // Disable some selected features
+ // for (String label : Arrays.asList(
+ // "ledger",
+ // "n11",
+ // "n20",
+ // "n02",
+ // "n30",
+ // "n21",
+ // "n12",
+ // "n03",
+ // "aspect")) {
+ // int p = ShapeDescription.getParameterIndex(label);
+ // setMinimum(p, shape, null);
+ // setMaximum(p, shape, null);
+ // }
+ //
+ // // Keep "stemNb" exactly as it is, with no margin
+ //
+ // // Third, remove some constraints
+ // switch (shape) {
+ // case TEXT :
+ // disableMaximum(TEXT, "weight");
+ // disableMaximum(TEXT, "width");
+ //
+ // break;
+ //
+ // case BRACE :
+ // disableMaximum(BRACE, "weight");
+ // disableMaximum(BRACE, "height");
+ //
+ // break;
+ //
+ // case BRACKET :
+ // disableMaximum(BRACKET, "weight");
+ // disableMaximum(BRACKET, "height");
+ //
+ // break;
+ //
+ // case BEAM :
+ // disableMaximum(BEAM, "weight");
+ // disableMaximum(BEAM, "width");
+ // disableMaximum(BEAM, "height");
+ //
+ // break;
+ //
+ // case BEAM_2 :
+ // disableMaximum(BEAM_2, "weight");
+ // disableMaximum(BEAM_2, "width");
+ // disableMaximum(BEAM_2, "height");
+ //
+ // break;
+ //
+ // case BEAM_3 :
+ // disableMaximum(BEAM_3, "weight");
+ // disableMaximum(BEAM_3, "width");
+ // disableMaximum(BEAM_3, "height");
+ //
+ // break;
+ //
+ // case SLUR :
+ // disableMaximum(SLUR, "weight");
+ // disableMaximum(SLUR, "width");
+ // disableMaximum(SLUR, "height");
+ //
+ // break;
+ //
+ // case ARPEGGIATO :
+ // disableMaximum(ARPEGGIATO, "weight");
+ // disableMaximum(ARPEGGIATO, "height");
+ //
+ // break;
+ //
+ // case CRESCENDO :
+ // disableMaximum(CRESCENDO, "weight");
+ // disableMaximum(CRESCENDO, "width");
+ // disableMaximum(CRESCENDO, "height");
+ //
+ // break;
+ //
+ // case DECRESCENDO :
+ // disableMaximum(DECRESCENDO, "weight");
+ // disableMaximum(DECRESCENDO, "width");
+ // disableMaximum(DECRESCENDO, "height");
+ //
+ // break;
+ //
+ // default :
+ // }
+ }
+
+ //----------------//
+ // disableMaximum //
+ //----------------//
+ private void disableMaximum (Shape shape,
+ String paramLabel)
+ {
+ setMaximum(ShapeDescription.getParameterIndex(paramLabel), shape, null);
+ }
+
+ //----------------//
+ // disableMinimum //
+ //----------------//
+ private void disableMinimum (Shape shape,
+ String paramLabel)
+ {
+ setMinimum(ShapeDescription.getParameterIndex(paramLabel), shape, null);
+ }
+
+ //------------//
+ // doGetRange //
+ //------------//
+ /**
+ * Retrieve (and create if necessary) the range entity that
+ * corresponds to parameter of paramIndex for the provided shape.
+ *
+ * @param paramIndex parameter reference
+ * @param shape provided shape
+ * @return the desired range entity
+ */
+ private Range doGetRange (int paramIndex,
+ Shape shape)
+ {
+ Range[] ranges = constraintMap.get(shape);
+
+ if (ranges == null) {
+ ranges = new Range[ShapeDescription.length()];
+ constraintMap.put(shape, ranges);
+ }
+
+ Range range = ranges[paramIndex];
+
+ if (range == null) {
+ ranges[paramIndex] = range = new Range();
+ }
+
+ return range;
+ }
+
+ //-----------------//
+ // firstMisMatched //
+ //-----------------//
+ /**
+ * Perform a basic check on max / min bounds, if any, for each
+ * parameter value of the provided pattern.
+ *
+ * @param pattern the collection of parameters to check with respect to
+ * targeted shape
+ * @param shape the targeted shape
+ * @return the name of the first failing check, null otherwise
+ */
+ private String firstMisMatched (double[] pattern,
+ Shape shape)
+ {
+ String[] labels = ShapeDescription.getParameterLabels();
+ Range[] ranges = constraintMap.get(shape);
+
+ if (ranges == null) {
+ return null; // No test => OK
+ }
+
+ for (int p = 0; p < ShapeDescription.length(); p++) {
+ String label = labels[p];
+ double val = pattern[p];
+ Range range = ranges[p];
+
+ if (range == null) {
+ continue;
+ }
+
+ Double min = range.min;
+
+ if ((min != null) && (val < min)) {
+ logger.debug("{} failed on minimum for {} {} < {}",
+ shape, label, val, min);
+ return label + ".min";
+ }
+
+ Double max = range.max;
+
+ if ((max != null) && (val > max)) {
+ logger.debug("{} failed on maximum for {} {} > {}",
+ shape, label, val, max);
+ return label + ".max";
+ }
+ }
+
+ // Everything is OK
+ return null;
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+ //-------//
+ // Range //
+ //-------//
+ public static class Range
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ /** Constraint on minimum, if any */
+ Double min;
+
+ /** Constraint on maximum, if any */
+ Double max;
+
+ }
+
+ //-----------//
+ // Constants //
+ //-----------//
+ private static final class Constants
+ extends ConstantSet
+ {
+ //~ Instance fields ----------------------------------------------------
+
+ Constant.Double factorForMinima = new Constant.Double(
+ "factor",
+ 0.7,
+ "Factor applied to all minimum constraints");
+
+ Constant.Double factorForMaxima = new Constant.Double(
+ "factor",
+ 1.3,
+ "Factor applied to all maximum constraints");
+
+ }
+}
diff --git a/src/main/omr/glyph/GlyphRepository.java b/src/main/omr/glyph/GlyphRepository.java
new file mode 100644
index 0000000..4994720
--- /dev/null
+++ b/src/main/omr/glyph/GlyphRepository.java
@@ -0,0 +1,1071 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h R e p o s i t o r y //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.WellKnowns;
+
+import omr.glyph.facets.BasicGlyph;
+import omr.glyph.facets.Glyph;
+import omr.glyph.facets.GlyphValue;
+
+import omr.lag.Section;
+
+import omr.sheet.Sheet;
+
+import omr.ui.symbol.MusicFont;
+import omr.ui.symbol.ShapeSymbol;
+import omr.ui.symbol.Symbols;
+
+import omr.util.BlackList;
+import omr.util.FileUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+
+/**
+ * Class {@code GlyphRepository} handles the store of known glyphs,
+ * across multiple sheets (and possibly multiple runs).
+ *
+ * A glyph is known by its full name, whose standard format is
+ * sheetName/Shape.id.xml , regardless of the area it is stored (this may
+ * be the core area or the global sheets area augmented by the
+ * samples area).
+ * It can also be an artificial glyph built from a symbol icon,
+ * in that case its full name is the similar formats icons/Shape.xml or
+ * icons/Shape.nn.xml where "nn" is a differentiating number.
+ *
+ *
The repository handles a private map of all deserialized glyphs so far,
+ * since the deserialization is a rather expensive operation.
+ *
+ *
It handles two bases : the "whole base" (all glyphs from sheets and
+ * samples folders) and the "core base" (just the glyphs of the core, which is
+ * built as a selected subset of the whole base).
+ * These bases are accessible respectively by {@link #getWholeBase} and
+ * {@link #getCoreBase} methods.
+ *
+ * @author Hervé Bitteur
+ */
+public class GlyphRepository
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(
+ GlyphRepository.class);
+
+ /** The single instance of this class */
+ private static volatile GlyphRepository INSTANCE;
+
+ /** Extension for training files */
+ private static final String FILE_EXTENSION = ".xml";
+
+ /** Extension for place-holder symbol files */
+ public static final String SYMBOL_EXTENSION = ".symbol";
+
+ /** Specific subdirectory for sheet glyphs */
+ private static final File sheetsFolder = new File(
+ WellKnowns.TRAIN_FOLDER,
+ "sheets");
+
+ /** Specific subdirectory for core glyphs */
+ private static final File coreFolder = new File(
+ WellKnowns.TRAIN_FOLDER,
+ "core");
+
+ /** Specific subdirectory for additional sample glyphs */
+ private static final File samplesFolder = new File(
+ WellKnowns.TRAIN_FOLDER,
+ "samples");
+
+ /** Specific filter for glyph files */
+ private static final FileFilter glyphFilter = new FileFilter()
+ {
+ @Override
+ public boolean accept (File file)
+ {
+ String ext = FileUtil.getExtension(file);
+
+ return file.isDirectory() || ext.equals(FILE_EXTENSION)
+ || ext.equals(SYMBOL_EXTENSION);
+ }
+ };
+
+ /** Un/marshalling context for use with JAXB */
+ private static volatile JAXBContext jaxbContext;
+
+ /** For comparing shape names */
+ public static final Comparator shapeComparator = new Comparator()
+ {
+ @Override
+ public int compare (String s1,
+ String s2)
+ {
+ String n1 = GlyphRepository.shapeNameOf(s1);
+ String n2 = GlyphRepository.shapeNameOf(s2);
+
+ return n1.compareTo(n2);
+ }
+ };
+
+ //~ Instance fields --------------------------------------------------------
+ /** Core collection of glyphs */
+ private volatile List coreBase;
+
+ /** Whole collection of glyphs */
+ private volatile List wholeBase;
+
+ /**
+ * Map of all glyphs deserialized so far, using full glyph name as
+ * key. Full glyph name format is : sheetName/Shape.id.xml
+ */
+ private final Map glyphsMap = new TreeMap<>();
+
+ /** Inverse map */
+ private final Map namesMap = new HashMap<>();
+
+ //~ Constructors -----------------------------------------------------------
+ /** Private singleton constructor */
+ private GlyphRepository ()
+ {
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //------------//
+ // fileNameOf //
+ //------------//
+ /**
+ * Report the file name w/o extension of a gName.
+ *
+ * @param gName glyph name, using format "folder/name.number.xml"
+ * or "folder/name.xml"
+ * @return the 'name' or 'name.number' part of the format
+ */
+ public static String fileNameOf (String gName)
+ {
+ int slash = gName.indexOf('/');
+ String nameWithExt = gName.substring(slash + 1);
+
+ int lastDot = nameWithExt.lastIndexOf('.');
+
+ if (lastDot != -1) {
+ return nameWithExt.substring(0, lastDot);
+ } else {
+ return nameWithExt;
+ }
+ }
+
+ //-------------//
+ // shapeNameOf //
+ //-------------//
+ /**
+ * Report the shape name of a gName.
+ *
+ * @param gName glyph name, using format "folder/name.number.xml" or
+ * "folder/name.xml"
+ * @return the 'name' part of the format
+ */
+ public static String shapeNameOf (String gName)
+ {
+ int slash = gName.indexOf('/');
+ String nameWithExt = gName.substring(slash + 1);
+
+ int firstDot = nameWithExt.indexOf('.');
+
+ if (firstDot != -1) {
+ return nameWithExt.substring(0, firstDot);
+ } else {
+ return nameWithExt;
+ }
+ }
+
+ //-------------//
+ // getCoreBase //
+ //-------------//
+ /**
+ * Return the names of the core collection of glyphs.
+ *
+ * @return the core collection of recorded glyphs
+ */
+ public List getCoreBase (Monitor monitor)
+ {
+ if (coreBase == null) {
+ synchronized (this) {
+ if (coreBase == null) {
+ coreBase = loadCoreBase(monitor);
+ }
+ }
+ }
+
+ return coreBase;
+ }
+
+ //----------//
+ // getGlyph //
+ //----------//
+ /**
+ * Return a glyph knowing its full glyph name, which is the name of
+ * the corresponding training material.
+ * If not already done, the glyph is deserialized from the training file,
+ * searching first in the icons area, then the train area.
+ *
+ * @param gName the full glyph name (format is: sheetName/Shape.id.xml)
+ * @param monitor the monitor, if any, to be kept informed of glyph loading
+ * @return the glyph instance if found, null otherwise
+ */
+ public synchronized Glyph getGlyph (String gName,
+ Monitor monitor)
+ {
+ // First, try the map of glyphs
+ Glyph glyph = glyphsMap.get(gName);
+
+ if (glyph == null) {
+ // If failed, actually load the glyph from XML backup file.
+ if (isIcon(gName)) {
+ glyph = buildSymbolGlyph(gName);
+ } else {
+ File file = new File(WellKnowns.TRAIN_FOLDER, gName);
+
+ if (!file.exists()) {
+ logger.warn("Unable to find file for glyph {}", gName);
+
+ return null;
+ }
+
+ glyph = buildGlyph(gName, file);
+ }
+
+ if (glyph != null) {
+ glyphsMap.put(gName, glyph);
+ namesMap.put(glyph, gName);
+ }
+
+ if (monitor != null) {
+ monitor.loadedGlyph(gName);
+ }
+ }
+
+ return glyph;
+ }
+
+ //--------------//
+ // getGlyphName //
+ //--------------//
+ public String getGlyphName (Glyph glyph)
+ {
+ return namesMap.get(glyph);
+ }
+
+ //-------------//
+ // getGlyphsIn //
+ //-------------//
+ /**
+ * Report the list of glyph files that are contained within a given
+ * directory
+ *
+ * @param dir the containing directory
+ * @return the list of glyph files
+ */
+ public synchronized List getGlyphsIn (File dir)
+ {
+ File[] files = listLegalFiles(dir);
+
+ if (files != null) {
+ return Arrays.asList(files);
+ } else {
+ logger.warn("Cannot get files list from dir {}", dir);
+
+ return new ArrayList<>();
+ }
+ }
+
+ //-------------//
+ // getInstance //
+ //-------------//
+ /**
+ * Report the single instance of this class, after creating it if
+ * needed.
+ *
+ * @return the single instance
+ */
+ public static GlyphRepository getInstance ()
+ {
+ if (INSTANCE == null) {
+ INSTANCE = new GlyphRepository();
+ }
+
+ return INSTANCE;
+ }
+
+ //----------------------//
+ // getSampleDirectories //
+ //----------------------//
+ /**
+ * Report the list of all samples directories found in the training
+ * material.
+ *
+ * @return the list of samples directories
+ */
+ public List getSampleDirectories ()
+ {
+ return getSubdirectories(samplesFolder);
+ }
+
+ //------------------//
+ // getSamplesFolder //
+ //------------------//
+ /**
+ * Report the folder where isolated samples glyphs are stored.
+ *
+ * @return the directory of isolated samples material
+ */
+ public File getSamplesFolder ()
+ {
+ return samplesFolder;
+ }
+
+ //---------------------//
+ // getSheetDirectories //
+ //---------------------//
+ /**
+ * Report the list of all sheet directories found in the training
+ * material.
+ *
+ * @return the list of sheet directories
+ */
+ public List getSheetDirectories ()
+ {
+ return getSubdirectories(sheetsFolder);
+ }
+
+ //-----------------//
+ // getSheetsFolder //
+ //-----------------//
+ /**
+ * Report the folder where all sheet glyphs are stored.
+ *
+ * @return the directory of all sheets material
+ */
+ public File getSheetsFolder ()
+ {
+ return sheetsFolder;
+ }
+
+ //--------------//
+ // getWholeBase //
+ //--------------//
+ /**
+ * Return the names of the whole collection of glyphs.
+ *
+ * @return the whole collection of recorded glyphs
+ */
+ public List getWholeBase (Monitor monitor)
+ {
+ if (wholeBase == null) {
+ synchronized (this) {
+ if (wholeBase == null) {
+ wholeBase = loadWholeBase(monitor);
+ }
+ }
+ }
+
+ return wholeBase;
+ }
+
+ //--------//
+ // isIcon //
+ //--------//
+ public boolean isIcon (String gName)
+ {
+ return isIcon(new File(gName));
+ }
+
+ //---------------//
+ // isIconsFolder //
+ //---------------//
+ public boolean isIconsFolder (String folder)
+ {
+ return folder.equals(WellKnowns.SYMBOLS_FOLDER.getName());
+ }
+
+ //----------//
+ // isLoaded //
+ //----------//
+ public synchronized boolean isLoaded (String gName)
+ {
+ return glyphsMap.get(gName) != null;
+ }
+
+ //----------------//
+ // recordOneGlyph //
+ //----------------//
+ /**
+ * Record one glyph on disk (into the samples folder).
+ *
+ * @param glyph the glyph to record
+ * @param sheet its containing sheet
+ */
+ public void recordOneGlyph (Glyph glyph,
+ Sheet sheet)
+ {
+ Shape shape = getRecordableShape(glyph);
+
+ if (shape != null) {
+ // Prepare target directory, based on sheet id
+ File sheetDir = new File(getSamplesFolder(), sheet.getId());
+
+ // Make sure related directory chain exists
+ if (sheetDir.mkdirs()) {
+ logger.info("Creating directory {}", sheetDir);
+ }
+
+ if (recordGlyph(glyph, shape, sheetDir) > 0) {
+ logger.info("Stored {} into {}", glyph.idString(), sheetDir);
+ }
+ } else {
+ logger.warn("Not recordable {}", glyph);
+ }
+ }
+
+ //-------------------//
+ // recordSheetGlyphs //
+ //-------------------//
+ /**
+ * Store all known glyphs of the provided sheet as separate XML
+ * files, so that they can be later reloaded to train an evaluator.
+ * We store glyph for which Shape is not null, and different from NOISE and
+ * STEM (CLUTTER is thus stored as well).
+ *
+ * STRUCTURE shapes are stored in a parallel sub-directory so that they
+ * don't get erased by shapes of their leaves.
+ *
+ * @param sheet the sheet whose glyphs are to be stored
+ * @param emptyStructures flag to specify if the Structure directory must be
+ * emptied beforehand
+ */
+ public void recordSheetGlyphs (Sheet sheet,
+ boolean emptyStructures)
+ {
+ // Prepare target directory
+ File sheetDir = new File(getSheetsFolder(), sheet.getId());
+
+ // Make sure related directory chain exists
+ if (sheetDir.mkdirs()) {
+ logger.info("Creating directory {}", sheetDir);
+ } else {
+ deleteXmlFiles(sheetDir);
+ }
+
+ // Now record each relevant glyph
+ int glyphNb = 0;
+
+ for (Glyph glyph : sheet.getActiveGlyphs()) {
+ Shape shape = getRecordableShape(glyph);
+
+ if (shape != null) {
+ glyphNb += recordGlyph(glyph, shape, sheetDir);
+ }
+ }
+
+ // Refresh glyph populations
+ refreshBases();
+
+ logger.info("{} glyphs stored from {}", glyphNb, sheet.getId());
+ }
+
+ //--------------//
+ // refreshBases //
+ //--------------//
+ public void refreshBases ()
+ {
+ wholeBase = null;
+ coreBase = null;
+ }
+
+ //-------------//
+ // removeGlyph //
+ //-------------//
+ /**
+ * Remove a glyph from the repository memory (this does not delete
+ * the actual glyph file on disk).
+ * We also remove it from the various bases which is safer.
+ *
+ * @param gName the full glyph name
+ */
+ public synchronized void removeGlyph (String gName)
+ {
+ glyphsMap.remove(gName);
+ refreshBases();
+ }
+
+ //-------------//
+ // setCoreBase //
+ //-------------//
+ /**
+ * Define the provided collection as the core training material.
+ *
+ * @param base the provided collection
+ */
+ public synchronized void setCoreBase (List base)
+ {
+ coreBase = base;
+ }
+
+ //---------//
+ // shapeOf //
+ //---------//
+ /**
+ * Infer the shape of a glyph directly from its full name.
+ *
+ * @param gName the full glyph name
+ * @return the shape of the known glyph
+ */
+ public Shape shapeOf (String gName)
+ {
+ return shapeOf(new File(gName));
+ }
+
+ //---------------//
+ // storeCoreBase //
+ //---------------//
+ /**
+ * Store the core training material.
+ */
+ public synchronized void storeCoreBase ()
+ {
+ if (coreBase == null) {
+ logger.warn("Core base is null");
+
+ return;
+ }
+
+ // Create the core directory if needed
+ coreFolder.mkdirs();
+
+ // Empty the directory
+ FileUtil.deleteAll(coreFolder.listFiles());
+
+ // Copy the glyph and icon files into the core directory
+ int copyNb = 0;
+
+ for (String gName : coreBase) {
+ final boolean isIcon = isIcon(gName);
+ final File source = isIcon
+ ? new File(
+ WellKnowns.SYMBOLS_FOLDER.getParentFile(),
+ gName) : new File(WellKnowns.TRAIN_FOLDER, gName);
+
+ final File target = new File(coreFolder, gName);
+ target.getParentFile().mkdirs();
+
+ logger.debug("Storing {} as core", target);
+
+ try {
+ if (isIcon) {
+ target.createNewFile();
+ } else {
+ FileUtil.copy(source, target);
+ }
+
+ copyNb++;
+ } catch (IOException ex) {
+ logger.warn("Cannot copy {} to {}", source, target);
+ }
+ }
+
+ logger.info("{} glyphs copied as core training material", copyNb);
+ }
+
+ //-----------------//
+ // unloadIconsFrom //
+ //-----------------//
+ public void unloadIconsFrom (List names)
+ {
+ for (String gName : names) {
+ if (isIcon(gName)) {
+ if (isLoaded(gName)) {
+ Glyph glyph = getGlyph(gName, null);
+
+ for (Section section : glyph.getMembers()) {
+ section.clearViews();
+ section.delete();
+ }
+ }
+
+ unloadGlyph(gName);
+ }
+ }
+ }
+
+ //-------------//
+ // unloadGlyph //
+ //-------------//
+ synchronized void unloadGlyph (String gName)
+ {
+ if (glyphsMap.containsKey(gName)) {
+ glyphsMap.remove(gName);
+ }
+ }
+
+ //------------//
+ // buildGlyph //
+ //------------//
+ private Glyph buildGlyph (String gName,
+ File file)
+ {
+ logger.debug("Loading glyph {}", file);
+
+ Glyph glyph = null;
+ InputStream is = null;
+
+ try {
+ is = new FileInputStream(file);
+ glyph = jaxbUnmarshal(is);
+ } catch (Exception ex) {
+ logger.warn("Could not unmarshal file {}", file);
+ ex.printStackTrace();
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ return glyph;
+ }
+
+ //------------------//
+ // buildSymbolGlyph //
+ //------------------//
+ /**
+ * Build an artificial glyph from a symbol descriptor, in order to
+ * train an evaluator even when we have no ground-truth glyph.
+ *
+ * @param gName path to the symbol descriptor on disk
+ * @return the glyph built, or null if failed
+ */
+ private Glyph buildSymbolGlyph (String gName)
+ {
+ Shape shape = shapeOf(gName);
+ Glyph glyph = null;
+
+ // Make sure we have the drawing available for this shape
+ ShapeSymbol symbol = Symbols.getSymbol(shape);
+
+ // If no plain symbol, use the decorated symbol as plan B
+ if (symbol == null) {
+ symbol = Symbols.getSymbol(shape, true);
+ }
+
+ if (symbol != null) {
+ logger.debug("Building symbol glyph {}", gName);
+
+ File file = new File(WellKnowns.TRAIN_FOLDER, gName);
+
+ if (file.exists()) {
+ try {
+ InputStream is = new FileInputStream(file);
+ SymbolGlyphDescriptor desc = SymbolGlyphDescriptor.
+ loadFromXmlStream(
+ is);
+ is.close();
+
+ logger.debug("Descriptor {}", desc);
+
+ glyph = new SymbolGlyph(
+ shape,
+ symbol,
+ MusicFont.DEFAULT_INTERLINE,
+ desc);
+ } catch (Exception ex) {
+ logger.warn("Cannot process " + file, ex);
+ }
+ }
+ } else {
+ //if (logger.isDebugEnabled()) {
+ logger.warn("No symbol for {}", gName);
+
+ //}
+ }
+
+ return glyph;
+ }
+
+ //----------------//
+ // deleteXmlFiles //
+ //----------------//
+ private void deleteXmlFiles (File dir)
+ {
+ File[] files = dir.listFiles();
+
+ for (File file : files) {
+ if (FileUtil.getExtension(file).equals(FILE_EXTENSION)) {
+ if (!file.delete()) {
+ logger.warn("Could not delete {}", file);
+ }
+ }
+ }
+ }
+
+ //----------------//
+ // getJaxbContext //
+ //----------------//
+ private JAXBContext getJaxbContext ()
+ throws JAXBException
+ {
+ // Lazy creation
+ if (jaxbContext == null) {
+ jaxbContext = JAXBContext.newInstance(GlyphValue.class);
+ }
+
+ return jaxbContext;
+ }
+
+ //--------------------//
+ // getRecordableShape //
+ //--------------------//
+ /**
+ * Report the shape to record for the provided glyph.
+ *
+ * @param glyph the provided glyph
+ * @return the precise shape to use, or null
+ */
+ private Shape getRecordableShape (Glyph glyph)
+ {
+ if ((glyph == null) || glyph.isVirtual() || (glyph.getShape() == null)) {
+ return null;
+ }
+
+ Shape shape = glyph.getShape().getPhysicalShape();
+
+ if (shape.isTrainable() && (shape != Shape.NOISE)) {
+ return shape;
+ } else {
+ return null;
+ }
+ }
+
+ //-------------------//
+ // getSubdirectories //
+ //-------------------//
+ private synchronized List getSubdirectories (File folder)
+ {
+ List dirs = new ArrayList<>();
+ File[] files = listLegalFiles(folder);
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ dirs.add(file);
+ }
+ }
+
+ return dirs;
+ }
+
+ //-------------//
+ // glyphNameOf //
+ //-------------//
+ /**
+ * Build the full glyph name (which will be the unique glyph name)
+ * from the file which contains the glyph description.
+ *
+ * @param file the glyph backup file
+ * @return the unique glyph name
+ */
+ private String glyphNameOf (File file)
+ {
+ if (isIcon(file)) {
+ return file.getParentFile().getName() + File.separator + file.
+ getName();
+ } else {
+ return file.getParentFile().getParentFile().getName() + File.separator
+ + file.getParentFile().getName() + File.separator + file.
+ getName();
+ }
+ }
+
+ //--------//
+ // isIcon //
+ //--------//
+ private boolean isIcon (File file)
+ {
+ String folder = file.getParentFile().getName();
+
+ return isIconsFolder(folder);
+ }
+
+ //-------------//
+ // jaxbMarshal //
+ //-------------//
+ private void jaxbMarshal (Glyph glyph,
+ OutputStream os)
+ throws JAXBException, Exception
+ {
+ Marshaller m = getJaxbContext().createMarshaller();
+ m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+ m.marshal(new GlyphValue(glyph), os);
+ }
+
+ //---------------//
+ // jaxbUnmarshal //
+ //---------------//
+ private Glyph jaxbUnmarshal (InputStream is)
+ throws JAXBException
+ {
+ Unmarshaller um = getJaxbContext().createUnmarshaller();
+ GlyphValue value = (GlyphValue) um.unmarshal(is);
+
+ return new BasicGlyph(value);
+ }
+
+ //----------------//
+ // listLegalFiles //
+ //----------------//
+ private File[] listLegalFiles (File dir)
+ {
+ return new BlackList(dir).listFiles(glyphFilter);
+ }
+
+ //----------//
+ // loadBase //
+ //----------//
+ /**
+ * Build the map and return the collection of glyphs names in a
+ * collection of directories.
+ *
+ * @param paths the array of paths to the directories to load
+ * @param monitor the observing entity if any
+ * @return the collection of loaded glyphs names
+ */
+ private synchronized List loadBase (File[] paths,
+ Monitor monitor)
+ {
+ // Files in the provided directory & its subdirectories
+ List files = new ArrayList<>(4000);
+
+ for (File path : paths) {
+ loadDirectory(path, files);
+ }
+
+ if (monitor != null) {
+ monitor.setTotalGlyphs(files.size());
+ }
+
+ // Now, collect the glyphs names
+ List base = new ArrayList<>(files.size());
+
+ for (File file : files) {
+ base.add(glyphNameOf(file));
+ }
+
+ logger.debug("{} glyphs names collected", files.size());
+
+ return base;
+ }
+
+ //--------------//
+ // loadCoreBase //
+ //--------------//
+ /**
+ * Build the collection of only the core glyphs.
+ *
+ * @return the collection of core glyphs names
+ */
+ private List loadCoreBase (Monitor monitor)
+ {
+ return loadBase(new File[]{coreFolder}, monitor);
+ }
+
+ //---------------//
+ // loadDirectory //
+ //---------------//
+ /**
+ * Retrieve recursively all files in the hierarchy starting at
+ * the given directory, and append them in the provided file list.
+ * If a black list exists in a directory, then all black-listed files
+ * (and direct sub-directories) hosted in this directory are skipped.
+ *
+ * @param dir the top directory where search is launched
+ * @param all the list to be augmented by found files
+ */
+ private void loadDirectory (File dir,
+ List all)
+ {
+ File[] files = listLegalFiles(dir);
+
+ logger.debug("Browsing directory {} total:{}", dir, files.length);
+
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ loadDirectory(file, all); // Recurse through it
+ } else {
+ all.add(file);
+ }
+ }
+ } else {
+ logger.warn("Directory {} is empty", dir);
+ }
+ }
+
+ //---------------//
+ // loadWholeBase //
+ //---------------//
+ /**
+ * Build the complete map of all glyphs recorded so far, beginning
+ * by the builtin icon glyphs, then the recorded glyphs
+ * (sheets & samples).
+ *
+ * @return a collection of (known) glyphs names
+ */
+ private List loadWholeBase (Monitor monitor)
+ {
+ return loadBase(
+ new File[]{WellKnowns.SYMBOLS_FOLDER, sheetsFolder,
+ samplesFolder},
+ monitor);
+ }
+
+ //-------------//
+ // recordGlyph //
+ //-------------//
+ /**
+ * Record a glyph, using the precise shape into the given directory.
+ *
+ * @param glyph the glyph to record
+ * @param shape the precise shape to use
+ * @param dir the target directory
+ * @return 1 if OK, 0 otherwise
+ */
+ private int recordGlyph (Glyph glyph,
+ Shape shape,
+ File dir)
+ {
+ OutputStream os = null;
+
+ try {
+ logger.debug("Storing {}", glyph);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(shape);
+ sb.append(".");
+ sb.append(String.format("%04d", glyph.getId()));
+ sb.append(FILE_EXTENSION);
+
+ File glyphFile;
+
+ glyphFile = new File(dir, sb.toString());
+
+ os = new FileOutputStream(glyphFile);
+ jaxbMarshal(glyph, os);
+
+ return 1;
+ } catch (Throwable ex) {
+ logger.warn("Error storing " + glyph, ex);
+ } finally {
+ try {
+ if (os != null) {
+ os.close();
+ }
+ } catch (IOException ex) {
+ logger.warn(null, ex);
+ }
+ }
+
+ return 0;
+ }
+
+ //---------//
+ // shapeOf //
+ //---------//
+ /**
+ * Infer the shape of a glyph directly from its file name.
+ *
+ * @param file the file that describes the glyph
+ * @return the shape of the known glyph
+ */
+ private Shape shapeOf (File file)
+ {
+ try {
+ // ex: ONE_32ND_REST.0105.xml (for real glyphs)
+ // ex: CODA.xml (for glyphs derived from icons)
+ String name = FileUtil.getNameSansExtension(file);
+ int dot = name.indexOf('.');
+
+ if (dot != -1) {
+ name = name.substring(0, dot);
+ }
+
+ return Shape.valueOf(name);
+ } catch (Exception ex) {
+ // Not recognized
+ return null;
+ }
+ }
+
+ //~ Inner Interfaces -------------------------------------------------------
+ //---------//
+ // Monitor //
+ //---------//
+ /**
+ * Interface {@code Monitor} defines the entries to a UI entity
+ * which monitors the loading of glyphs by the glyph repository.
+ */
+ public static interface Monitor
+ {
+ //~ Methods ------------------------------------------------------------
+
+ /**
+ * Called whenever a new glyph has been loaded.
+ *
+ * @param gName the normalized glyph name
+ */
+ void loadedGlyph (String gName);
+
+ /**
+ * Called to pass the number of selected glyphs, which will be
+ * later loaded.
+ *
+ * @param selected the size of the selection
+ */
+ void setSelectedGlyphs (int selected);
+
+ /**
+ * Called to pass the total number of available glyph
+ * descriptions in the training material.
+ *
+ * @param total the size of the training material
+ */
+ void setTotalGlyphs (int total);
+ }
+}
diff --git a/src/main/omr/glyph/GlyphSignature.java b/src/main/omr/glyph/GlyphSignature.java
new file mode 100644
index 0000000..d48962a
--- /dev/null
+++ b/src/main/omr/glyph/GlyphSignature.java
@@ -0,0 +1,145 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h S i g n a t u r e //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.glyph.facets.Glyph;
+
+import omr.moments.GeometricMoments;
+
+/**
+ * Class {@code GlyphSignature} is used to implement a map of glyphs,
+ * based only on their physical properties.
+ *
+ * The signature is implemented using the glyph moments.
+ *
+ * @author Hervé Bitteur
+ */
+public class GlyphSignature
+ implements Comparable
+{
+ //~ Instance fields --------------------------------------------------------
+
+ /** Glyph absolute weight */
+ private final int weight;
+
+ /** Glyph normalized moments */
+ private GeometricMoments moments;
+
+ //~ Constructors -----------------------------------------------------------
+ //----------------//
+ // GlyphSignature //
+ //----------------//
+ /**
+ * Creates a new GlyphSignature object.
+ *
+ * @param glyph the glyph to compute signature upon
+ */
+ public GlyphSignature (Glyph glyph)
+ {
+ weight = glyph.getWeight();
+ moments = new GeometricMoments(glyph.getGeometricMoments());
+ }
+
+ //----------------//
+ // GlyphSignature //
+ //----------------//
+ /**
+ * Needed by JAXB.
+ */
+ private GlyphSignature ()
+ {
+ weight = 0;
+ moments = null;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //-----------//
+ // compareTo //
+ //-----------//
+ @Override
+ public int compareTo (GlyphSignature other)
+ {
+ if (weight < other.weight) {
+ return -1;
+ } else if (weight > other.weight) {
+ return 1;
+ }
+
+ final Double[] values = moments.getValues();
+ final Double[] otherValues = other.moments.getValues();
+
+ for (int i = 0; i < values.length; i++) {
+ int cmp = Double.compare(values[i], otherValues[i]);
+
+ if (cmp != 0) {
+ return cmp;
+ }
+ }
+
+ return 0; // Equal
+ }
+
+ //--------//
+ // equals //
+ //--------//
+ @Override
+ public boolean equals (Object obj)
+ {
+ if (obj == this) {
+ return true;
+ }
+
+ if (obj instanceof GlyphSignature) {
+ return compareTo((GlyphSignature) obj) == 0;
+ } else {
+ return false;
+ }
+ }
+
+ //-----------//
+ // getWeight //
+ //-----------//
+ public int getWeight ()
+ {
+ return weight;
+ }
+
+ //----------//
+ // hashCode //
+ //----------//
+ @Override
+ public int hashCode ()
+ {
+ int hash = 7;
+ hash = (41 * hash) + this.weight;
+
+ return hash;
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ @Override
+ public String toString ()
+ {
+ StringBuilder sb = new StringBuilder("{GSig");
+
+ sb.append(" weight=")
+ .append(weight);
+
+ sb.append(" moments=")
+ .append(moments);
+ sb.append("}");
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/omr/glyph/Glyphs.java b/src/main/omr/glyph/Glyphs.java
new file mode 100644
index 0000000..826788c
--- /dev/null
+++ b/src/main/omr/glyph/Glyphs.java
@@ -0,0 +1,663 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h s //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.glyph.facets.BasicAlignment;
+import omr.glyph.facets.Glyph;
+
+import omr.lag.Section;
+
+import omr.math.PointsCollector;
+
+import omr.run.Orientation;
+
+import omr.sheet.Scale;
+
+import omr.util.Predicate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Polygon;
+import java.awt.Rectangle;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Class {@code Glyphs} is a collection of static convenient methods,
+ * providing features related to a collection of glyphs.
+ *
+ * @author Hervé Bitteur
+ */
+public class Glyphs
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(Glyphs.class);
+
+ /** Predicate to check for a manual shape */
+ public static final Predicate manualPredicate = new Predicate()
+ {
+ @Override
+ public boolean check (Glyph glyph)
+ {
+ return glyph.isManualShape();
+ }
+ };
+
+ /** Predicate to check for a barline shape */
+ public static final Predicate barPredicate = new Predicate()
+ {
+ @Override
+ public boolean check (Glyph glyph)
+ {
+ return glyph.isBar();
+ }
+ };
+
+ /** A immutable empty set of glyphs */
+ public static final Set NO_GLYPHS = Collections.emptySet();
+
+ //~ Methods ----------------------------------------------------------------
+ //----------//
+ // contains //
+ //----------//
+ /**
+ * Check whether a collection of glyphs contains at least one glyph
+ * for which the provided predicate holds true.
+ *
+ * @param glyphs the glyph collection to check
+ * @param predicate the predicate to be used
+ * @return true if there is at least one matching glyph
+ */
+ public static boolean contains (Collection glyphs,
+ Predicate predicate)
+ {
+ return firstOf(glyphs, predicate) != null;
+ }
+
+ //-----------------//
+ // containsBarline //
+ //-----------------//
+ /**
+ * Check whether the collection of glyphs contains at least one
+ * barline.
+ *
+ * @param glyphs the collection to check
+ * @return true if one or several glyphs are barlines components
+ */
+ public static boolean containsBarline (Collection glyphs)
+ {
+ return firstOf(glyphs, barPredicate) != null;
+ }
+
+ //------------//
+ // containsId //
+ //------------//
+ public static boolean containsId (Collection glyphs,
+ int id)
+ {
+ for (Glyph glyph : glyphs) {
+ if (glyph.getId() == id) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ //----------------//
+ // containsManual //
+ //----------------//
+ /**
+ * Check whether a collection of glyphs contains at least one glyph
+ * with a manually assigned shape.
+ *
+ * @param glyphs the glyph collection to check
+ * @return true if there is at least one manually assigned shape
+ */
+ public static boolean containsManual (Collection glyphs)
+ {
+ return contains(glyphs, manualPredicate);
+ }
+
+ //---------//
+ // firstOf //
+ //---------//
+ /**
+ * Report the first glyph, if any, for which the provided predicate
+ * holds true.
+ *
+ * @param glyphs the glyph collection to check
+ * @param predicate the glyph predicate
+ * @return the first matching glyph found if any, null otherwise
+ */
+ public static Glyph firstOf (Collection glyphs,
+ Predicate predicate)
+ {
+ for (Glyph glyph : glyphs) {
+ if (predicate.check(glyph)) {
+ return glyph;
+ }
+ }
+
+ return null;
+ }
+
+ //-----------//
+ // getBounds //
+ //-----------//
+ /**
+ * Return the display bounding box of a collection of glyphs.
+ *
+ * @param glyphs the provided collection of glyphs
+ * @return the bounding contour
+ */
+ public static Rectangle getBounds (Collection glyphs)
+ {
+ Rectangle box = null;
+
+ for (Glyph glyph : glyphs) {
+ if (box == null) {
+ box = new Rectangle(glyph.getBounds());
+ } else {
+ box.add(glyph.getBounds());
+ }
+ }
+
+ return box;
+ }
+
+ //----------------------------//
+ // getReverseLengthComparator //
+ //----------------------------//
+ /**
+ * For comparing glyph instances on decreasing length.
+ *
+ * @param orientation the desired orientation reference
+ * @return the comparator
+ */
+ public static Comparator getReverseLengthComparator (
+ final Orientation orientation)
+ {
+ return new Comparator()
+ {
+ @Override
+ public int compare (Glyph s1,
+ Glyph s2)
+ {
+ return s2.getLength(orientation)
+ - s1.getLength(orientation);
+ }
+ };
+ }
+
+ //----------------//
+ // getThicknessAt //
+ //----------------//
+ /**
+ * Report the resulting thickness of the collection of sticks at
+ * the provided coordinate.
+ *
+ * @param coord the desired coordinate
+ * @param orientation the desired orientation reference
+ * @param glyphs glyphs contributing to the resulting thickness
+ * @return the thickness measured, expressed in number of pixels.
+ */
+ public static double getThicknessAt (double coord,
+ Orientation orientation,
+ Glyph... glyphs)
+ {
+ return getThicknessAt(coord, orientation, null, glyphs);
+ }
+
+ //----------------//
+ // getThicknessAt //
+ //----------------//
+ /**
+ * Report the resulting thickness of the collection of sticks at
+ * the provided coordinate.
+ *
+ * @param coord the desired coordinate
+ * @param orientation the desired orientation reference
+ * @param section section contributing to the resulting thickness
+ * @param glyphs glyphs contributing to the resulting thickness
+ * @return the thickness measured, expressed in number of pixels.
+ */
+ public static double getThicknessAt (double coord,
+ Orientation orientation,
+ Section section,
+ Glyph... glyphs)
+ {
+ if (glyphs.length == 0) {
+ if (section == null) {
+ return 0;
+ } else {
+ return section.getMeanThickness(orientation);
+ }
+ }
+
+ // Retrieve global bounds
+ Rectangle absBox = null;
+
+ if (section != null) {
+ absBox = section.getBounds();
+ }
+
+ for (Glyph g : glyphs) {
+ if (absBox == null) {
+ absBox = g.getBounds();
+ } else {
+ absBox.add(g.getBounds());
+ }
+ }
+
+ Rectangle oBox = orientation.oriented(absBox);
+ int intCoord = (int) Math.floor(coord);
+
+ if ((intCoord < oBox.x) || (intCoord >= (oBox.x + oBox.width))) {
+ return 0;
+ }
+
+ // Use a large-enough collector
+ final Rectangle oRoi = new Rectangle(intCoord, oBox.y, 0, oBox.height);
+ final Scale scale = new Scale(glyphs[0].getInterline());
+ final int probeHalfWidth = scale.toPixels(
+ BasicAlignment.getProbeWidth()) / 2;
+ oRoi.grow(probeHalfWidth, 0);
+
+ PointsCollector collector = new PointsCollector(
+ orientation.absolute(oRoi));
+
+ // Collect sections contribution
+ for (Glyph g : glyphs) {
+ for (Section sct : g.getMembers()) {
+ sct.cumulate(collector);
+ }
+ }
+
+ // Contributing section, if any
+ if (section != null) {
+ section.cumulate(collector);
+ }
+
+ // Case of no pixels found
+ if (collector.getSize() == 0) {
+ return 0;
+ }
+
+ // Analyze range of Y values
+ int minVal = Integer.MAX_VALUE;
+ int maxVal = Integer.MIN_VALUE;
+ int[] vals = (orientation == Orientation.HORIZONTAL)
+ ? collector.getYValues() : collector.getXValues();
+
+ for (int i = 0, iBreak = collector.getSize(); i < iBreak; i++) {
+ int val = vals[i];
+ minVal = Math.min(minVal, val);
+ maxVal = Math.max(maxVal, val);
+ }
+
+ return maxVal - minVal + 1;
+ }
+
+ //----------//
+ // glyphsOf //
+ //----------//
+ /**
+ * Report the set of glyphs that are pointed back by the provided
+ * collection of sections.
+ *
+ * @param sections the provided sections
+ * @return the set of active containing glyphs
+ */
+ public static Set glyphsOf (Collection sections)
+ {
+ Set glyphs = new LinkedHashSet<>();
+
+ for (Section section : sections) {
+ Glyph glyph = section.getGlyph();
+
+ if (glyph != null) {
+ glyphs.add(glyph);
+ }
+ }
+
+ return glyphs;
+ }
+
+ //--------------//
+ // lookupGlyphs //
+ //--------------//
+ /**
+ * Look up in a collection of glyphs for all glyphs
+ * contained in a provided rectangle.
+ *
+ * @param collection the collection of glyphs to be browsed
+ * @param rect the coordinates rectangle
+ * @return the glyphs found, which may be an empty list
+ */
+ public static Set lookupGlyphs (
+ Collection extends Glyph> collection,
+ Rectangle rect)
+ {
+ Set set = new LinkedHashSet<>();
+
+ for (Glyph glyph : collection) {
+ if (rect.contains(glyph.getBounds())) {
+ set.add(glyph);
+ }
+ }
+
+ return set;
+ }
+
+ //--------------//
+ // lookupGlyphs //
+ //--------------//
+ /**
+ * Look up in a collection of glyphs for all glyphs
+ * contained in a provided polygon.
+ *
+ * @param collection the collection of glyphs to be browsed
+ * @param polygon the containing polygon
+ * @return the glyphs found, which may be an empty list
+ */
+ public static Set lookupGlyphs (
+ Collection extends Glyph> collection,
+ Polygon polygon)
+ {
+ Set set = new LinkedHashSet<>();
+
+ for (Glyph glyph : collection) {
+ if (polygon.contains(glyph.getBounds())) {
+ set.add(glyph);
+ }
+ }
+
+ return set;
+ }
+
+ //--------------//
+ // lookupGlyphs //
+ //--------------//
+ /**
+ * Look up in a collection of glyphs for all glyphs
+ * compatible with a provided predicate.
+ *
+ * @param collection the collection of glyphs to be browsed
+ * @param predicate the predicate to apply to each candidate (a null
+ * predicate will accept all candidates)
+ * @return the glyphs found, which may be an empty list
+ */
+ public static Set lookupGlyphs (
+ Collection extends Glyph> collection,
+ Predicate predicate)
+ {
+ Set set = new LinkedHashSet<>();
+
+ for (Glyph glyph : collection) {
+ if ((predicate == null) || predicate.check(glyph)) {
+ set.add(glyph);
+ }
+ }
+
+ return set;
+ }
+
+ //-------------------------//
+ // lookupIntersectedGlyphs //
+ //-------------------------//
+ /**
+ * Look up in a collection of glyphs for all glyphs
+ * intersected by a provided rectangle.
+ *
+ * @param collection the collection of glyphs to be browsed
+ * @param rect the coordinates rectangle
+ * @return the glyphs found, which may be an empty list
+ */
+ public static Set lookupIntersectedGlyphs (
+ Collection extends Glyph> collection,
+ Rectangle rect)
+ {
+ Set set = new LinkedHashSet<>();
+
+ for (Glyph glyph : collection) {
+ if (rect.intersects(glyph.getBounds())) {
+ set.add(glyph);
+ }
+ }
+
+ return set;
+ }
+
+ //-------//
+ // purge //
+ //-------//
+ /**
+ * Purge a collection of glyphs of those which match the given
+ * predicate.
+ *
+ * @param glyphs the glyph collection to purge
+ * @param predicate the predicate to detect glyphs to purge
+ */
+ public static void purge (Collection glyphs,
+ Predicate predicate)
+ {
+ if (predicate == null) {
+ return;
+ }
+
+ for (Iterator it = glyphs.iterator(); it.hasNext();) {
+ Glyph glyph = it.next();
+
+ if (predicate.check(glyph)) {
+ it.remove();
+ }
+ }
+ }
+
+ //--------------//
+ // purgeManuals //
+ //--------------//
+ /**
+ * Purge a collection of glyphs of those which exhibit a manually
+ * assigned shape.
+ *
+ * @param glyphs the glyph collection to purge
+ */
+ public static void purgeManuals (Collection glyphs)
+ {
+ for (Iterator it = glyphs.iterator(); it.hasNext();) {
+ Glyph glyph = it.next();
+
+ if (glyph.isManualShape()) {
+ it.remove();
+ }
+ }
+ }
+
+ //------------//
+ // sectionsOf //
+ //------------//
+ /**
+ * Report the set of sections contained by the provided collection
+ * of glyphs.
+ *
+ * @param glyphs the provided glyphs
+ * @return the set of all member sections
+ */
+ public static Set sectionsOf (Collection glyphs)
+ {
+ Set sections = new TreeSet<>();
+
+ for (Glyph glyph : glyphs) {
+ sections.addAll(glyph.getMembers());
+ }
+
+ return sections;
+ }
+
+ //----------//
+ // shapesOf //
+ //----------//
+ /**
+ * Report the set of shapes that appear in at least one of the
+ * provided glyphs.
+ *
+ * @param glyphs the provided collection of glyphs
+ * @return the shapes assigned among these glyphs
+ */
+ public static Set shapesOf (Collection glyphs)
+ {
+ EnumSet shapes = EnumSet.noneOf(Shape.class);
+
+ if (glyphs != null) {
+ for (Glyph glyph : glyphs) {
+ if (glyph.getShape() != null) {
+ shapes.add(glyph.getShape());
+ }
+ }
+ }
+
+ return shapes;
+ }
+
+ //-----------//
+ // sortedSet //
+ //-----------//
+ /**
+ * Build a mutable set with the provided glyphs.
+ *
+ * @param glyphs the provided glyphs
+ * @return a mutable sorted set composed of these glyphs
+ */
+ public static SortedSet sortedSet (Glyph... glyphs)
+ {
+ SortedSet set = new TreeSet<>(Glyph.byAbscissa);
+
+ if (glyphs.length > 0) {
+ set.addAll(Arrays.asList(glyphs));
+ }
+
+ return set;
+ }
+
+ //-----------//
+ // sortedSet //
+ //-----------//
+ /**
+ * Build a mutable set with the provided glyphs.
+ *
+ * @param glyphs the provided glyphs
+ * @return a mutable sorted set composed of these glyphs
+ */
+ public static SortedSet sortedSet (Collection glyphs)
+ {
+ SortedSet set = new TreeSet<>(Glyph.byAbscissa);
+ set.addAll(glyphs);
+
+ return set;
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Build a string with just the ids of the glyph collection,
+ * introduced by the provided label.
+ *
+ * @param label the string that introduces the list of IDs
+ * @param glyphs the collection of glyphs
+ * @return the string built
+ */
+ public static String toString (String label,
+ Collection extends Glyph> glyphs)
+ {
+ if (glyphs == null) {
+ return "";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(label)
+ .append("[");
+
+ for (Glyph glyph : glyphs) {
+ sb.append("#")
+ .append(glyph.getId());
+ }
+
+ sb.append("]");
+
+ return sb.toString();
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Build a string with just the ids of the glyph array, introduced
+ * by the provided label.
+ *
+ * @param label the string that introduces the list of IDs
+ * @param glyphs the array of glyphs
+ * @return the string built
+ */
+ public static String toString (String label,
+ Glyph... glyphs)
+ {
+ return toString(label, Arrays.asList(glyphs));
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Build a string with just the ids of the glyph collection,
+ * introduced by the label "glyphs".
+ *
+ * @param glyphs the collection of glyphs
+ * @return the string built
+ */
+ public static String toString (Collection extends Glyph> glyphs)
+ {
+ return toString("glyphs", glyphs);
+ }
+
+ //----------//
+ // toString //
+ //----------//
+ /**
+ * Build a string with just the ids of the glyph array, introduced
+ * by the label "glyphs".
+ *
+ * @param glyphs the array of glyphs
+ * @return the string built
+ */
+ public static String toString (Glyph... glyphs)
+ {
+ return toString("glyphs", glyphs);
+ }
+
+ private Glyphs ()
+ {
+ }
+}
diff --git a/src/main/omr/glyph/GlyphsBuilder.java b/src/main/omr/glyph/GlyphsBuilder.java
new file mode 100644
index 0000000..99c4b1e
--- /dev/null
+++ b/src/main/omr/glyph/GlyphsBuilder.java
@@ -0,0 +1,583 @@
+//----------------------------------------------------------------------------//
+// //
+// G l y p h s B u i l d e r //
+// //
+//----------------------------------------------------------------------------//
+// //
+// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
+// This software is released under the GNU General Public License. //
+// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
+//----------------------------------------------------------------------------//
+//
+package omr.glyph;
+
+import omr.constant.ConstantSet;
+
+import omr.glyph.facets.BasicGlyph;
+import omr.glyph.facets.Glyph;
+
+import omr.lag.Section;
+
+import omr.score.entity.Staff;
+
+import omr.sheet.Scale;
+import omr.sheet.Sheet;
+import omr.sheet.SystemInfo;
+
+import omr.util.HorizontalSide;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Class {@code GlyphsBuilder} is, at a system level, in charge of
+ * building (and removing) glyphs and of updating accordingly the
+ * containing entities (Nest and SystemInfo).
+ *
+ * It does not handle the shape of a glyph (this higher-level task is
+ * handled by {@link GlyphInspector} among others).
+ * But it does handle all the physical characteristics of a glyph via {@link
+ * #computeGlyphFeatures} (moments, plus additional data such as ledger, stem).
+ *
+ *
It typically handles via {@link #retrieveGlyphs} the building of glyphs
+ * out of the remaining sections of a sheet (since this is done using the
+ * physical edges between the sections).
+ *
+ *
It provides provisioning methods to actually insert or remove a glyph:
+ *
+ *
+ * A given newly built glyph can be inserted via {@link #addGlyph}
+ *
+ * Similarly {@link #removeGlyph} allows the removal of an existing glyph.
+ * Nota: Remember that the sections that compose a glyph are not removed,
+ * only the glyph is removed. The link from the contained sections back to the
+ * containing glyph is set to null.
+ *
+ *
+ * @author Hervé Bitteur
+ */
+public class GlyphsBuilder
+{
+ //~ Static fields/initializers ---------------------------------------------
+
+ /** Specific application parameters */
+ private static final Constants constants = new Constants();
+
+ /** Usual logger utility */
+ private static final Logger logger = LoggerFactory.getLogger(GlyphsBuilder.class);
+
+ //~ Instance fields --------------------------------------------------------
+ /** The dedicated system */
+ private final SystemInfo system;
+
+ /** The global sheet scale */
+ private final Scale scale;
+
+ /** Global hosting nest for glyphs */
+ private final Nest nest;
+
+ /** Margins for a stem */
+ private final int stemXMargin;
+
+ private final int stemYMargin;
+
+ //~ Constructors -----------------------------------------------------------
+ //---------------//
+ // GlyphsBuilder //
+ //---------------//
+ /**
+ * Creates a system-dedicated builder of glyphs.
+ *
+ * @param system the dedicated system
+ */
+ public GlyphsBuilder (SystemInfo system)
+ {
+ this.system = system;
+
+ Sheet sheet = system.getSheet();
+ scale = sheet.getScale();
+ nest = sheet.getNest();
+
+ // Cache parameters
+ stemXMargin = scale.toPixels(constants.stemXMargin);
+ stemYMargin = scale.toPixels(constants.stemYMargin);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+ //------------//
+ // buildGlyph //
+ //------------//
+ /**
+ * Build a glyph from a collection of sections, with a link back
+ * from the sections to the glyph.
+ *
+ * @param scale the context scale
+ * @param sections the provided members of the future glyph
+ * @return the newly built glyph
+ */
+ public static Glyph buildGlyph (Scale scale,
+ Collection sections)
+ {
+ Glyph glyph = new BasicGlyph(scale.getInterline());
+
+ for (Section section : sections) {
+ glyph.addSection(section, Glyph.Linking.LINK_BACK);
+ }
+
+ return glyph;
+ }
+
+ //----------------//
+ // retrieveGlyphs //
+ //----------------//
+ /**
+ * Browse through the provided sections not assigned to known
+ * glyphs, and build new glyphs out of connected sections.
+ *
+ * @param sections the sections to browse
+ * @param nest the nest to host glyphs
+ * @param scale the sheet scale
+ */
+ public static List retrieveGlyphs (List sections,
+ Nest nest,
+ Scale scale)
+ {
+ List created = new ArrayList<>();
+
+ // Reset section processed flag
+ for (Section section : sections) {
+ if (!section.isKnown()) {
+ section.setProcessed(false);
+ } else {
+ section.setProcessed(true);
+ }
+ }
+
+ // Browse the various unrecognized sections
+ for (Section section : sections) {
+ // Not already visited ?
+ if (!section.isProcessed()) {
+ // Let's build a new glyph around this starting section
+ Glyph glyph = new BasicGlyph(scale.getInterline());
+ considerConnection(glyph, section);
+
+ // Insert this newly built glyph into nest (no system invloved)
+ glyph = nest.addGlyph(glyph);
+ created.add(glyph);
+ }
+ }
+
+ return created;
+ }
+
+ //----------//
+ // addGlyph //
+ //----------//
+ /**
+ * Add a brand new glyph as an active glyph in proper system and nest.
+ * 'Active' means that all member sections are set to point back to the
+ * containing glyph.
+ *
+ * @param glyph the brand new glyph
+ * @return the original glyph as inserted in the glyph nest
+ */
+ public Glyph addGlyph (Glyph glyph)
+ {
+ glyph = nest.addGlyph(glyph);
+
+ system.addToGlyphsCollection(glyph);
+
+ return glyph;
+ }
+
+ //------------//
+ // buildGlyph //
+ //------------//
+ /**
+ * Build a glyph from a collection of sections, with a link back
+ * from the sections to the glyph, using the system scale.
+ *
+ * @param sections the provided members of the future glyph
+ * @return the newly built glyph
+ */
+ public Glyph buildGlyph (Collection sections)
+ {
+ return buildGlyph(scale, sections);
+ }
+
+ //------------------------//
+ // buildTransientCompound //
+ //------------------------//
+ /**
+ * Make a new transient glyph out of a collection of (sub) glyphs,
+ * by merging all their member sections.
+ *
+ * @param parts the collection of (sub) glyphs
+ * @return the brand new (compound) glyph
+ */
+ public Glyph buildTransientCompound (Collection parts)
+ {
+ // Gather all the sections involved
+ Collection