//----------------------------------------------------------------------------//
// //
// C o n s t a n t M a n a g e r //
// //
//----------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr"> //
// 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. //
//----------------------------------------------------------------------------//
// </editor-fold>
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.
*
* <p/>
* 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:
*
* <ol> <li> First, <b>SOURCE</b> values are always provided within
* <em><b>source declaration</b></em> of the constants in the Java source file
* itself.
* For example, in the <em><b>"omr/sheet/ScaleBuilder.java"</b></em> 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).
*
* <pre>
* Constant.Integer minResolution = new Constant.Integer(
* "Pixels",
* 11,
* "Minimum resolution, expressed as number of pixels per interline");
* </pre>This declaration must be read as follows:<ul>
*
* <li>{@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</li>
*
* <li>{@code "Pixels"} specifies the unit used. Here we are counting in
* pixels.</li>
*
* <li>{@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.</li>
*
* <li><code>"Minimum resolution, expressed as number of pixels per interline"
* </code> is the constant description, which will be used as a tool tip in
* the GUI interface in charge of editing these constants.</li></ul>
*
* </li><br/>
*
* <li>Then, <b>USER</b> values, contained in a property file named
* <em><b>"run.properties"</b></em> 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:
* <pre>
* omr.sheet.ScaleBuilder.minInterline=12</pre>
* 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.</li>
* <br/>
*
* <li>Then, <b>CLI</b> values, as set on the command line interface, by means
* of the <em><b>"-option"</b> key=value</em> command. For further details on
* this command, refer to the {@link omr.CLI} class documentation.
* <br/>Persistency here depends on the way Audiveris is running:<ul>
* <li>When running in <i>batch</i> mode, these CLI-defined constant values
* <b>are not</b> persisted in the USER file, unless the constant
* {@code omr.Main.persistBatchCliConstants} is set to true.</li>
* <li>When running in <i>interactive</i> mode, these CLI-defined constant
* values <b>are</b> always persisted in the USER file.</li></ul></li> <br/>
*
* <li>Finally, <b>UI Options Menu</b> values, as set online through the
* graphical user interface. These constant values defined at the GUI level are
* persisted in the USER file.</li> </ol>
*
* <p> 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, <b>provided the application is normally closed rather than
* killed</b>. It can also be stored programmatically by calling the
* {@link #storeResource} method.
*
* <p> 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.</p>
*
* @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<String, Constant> 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<String> getAllProperties ()
{
SortedSet<String> 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<String> 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<String> getKeys ()
{
Collection<String> 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<String> getUnusedKeys ()
{
SortedSet<String> props = new TreeSet<>();
for (Object obj : properties.keySet()) {
if (!constants.containsKey((String) obj)) {
props.add((String) obj);
}
}
return props;
}
public Collection<String> getUselessKeys ()
{
SortedSet<String> props = new TreeSet<>();
for (Entry<Object, Object> 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<String, Constant> 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());
}
}
}
}