Config.java
/*
* ..::jDrawingLib::..
*
* Copyright (C) Federico Vera 2012 - 2023 <[email protected]>
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or any later
* version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.dkt.graphics.utils.config;
import java.awt.Color;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
import java.util.Set;
import javax.swing.ImageIcon;
/**
* This class represents a generic Configuration class, with change listeners.
* This class supports a {@link ConfigListener} that is fired when key-values
* are updated, added or removed on a <i>per config</i> basis.
*
* @author Federico Vera {@literal <dktcoding [at] gmail>}
*/
public class Config implements Serializable {
@Serial
private static final long serialVersionUID = 6450068842696620740L;
private static final HashMap<String, Config> CONFIGS = new HashMap<>(8);
private final HashMap<String, Object> data = new HashMap<>(16);
private final ArrayList<ConfigListener> listeners = new
ArrayList<>(4);
/**
* Don't let anyone else initialize this class
*/
private Config () {
}
/**
* A wrapper for {@link Config#on(String)}
*
* @param name config name
* @return {@code Config} object
* @see Config#on(String)
*/
public static Config from(String name) {
return on(name);
}
/**
* Retrieves a {@code Config} instance associated with a given {@code name},
* this is usually the module/plugin name.<br>
* <pre>
*
* Config myApp = Config.on("my.config");
* myApp.set("key.1", "value.1");
* ...
* </pre>
*
* @param name config name
* @return {@code Config} object
*/
public static Config on(String name) {
synchronized(CONFIGS) {
if (!CONFIGS.containsKey(name)) {
CONFIGS.put(name, new Config());
}
return CONFIGS.get(name);
}
}
/**
* Destroys an instance of {@code Config} for a given {@code name}, this
* is useful when you have more than one application using this Class, and
* they don't start and stop at the same time.
*
* @param name config name
*/
public static void remove(String name) {
synchronized(CONFIGS) {
final Config conf = CONFIGS.remove(name);
if (conf != null) {
conf.listeners.clear();
conf.data.clear();
}
}
}
/**
* Retrieves a {@link Set} with all the available {@link Config} objects
* @return Set of configs
*/
public static Set<String> configSet() {
synchronized(CONFIGS) {
return CONFIGS.keySet();
}
}
/**
* Retrieves the number of created {@link Config} objects.
* @return number of configs
*/
public static int size() {
return CONFIGS.size();
}
/**
* A wrapper for {@code Config.put(String,Object)}
*
* @param key {@code key}
* @param value {@code value}
* @see Config#put(String,Object)
* @see Config#addListener(ConfigListener)
*/
public void set(String key, Object value) {
put(key, value);
}
/**
* Set's a config {@code key,value} pair. This values can be exported and
* imported.<br>
* <i>Note: </i> every <b>change</b> in this values will trigger a {@link
* ConfigEvent} on all the registered {@link ConfigListener}s.
*
* @param key {@code key}
* @param value {@code value}
* @see Config#addListener(ConfigListener)
*/
public void put(String key, Object value) {
synchronized (data) {
final Object old = data.get(key);
if (value == null){
data.remove(key);
} else {
data.put(key, value);
}
fireEvent(key, old, value);
}
}
/**
* Retrieves a raw {@code value} for a given {@code key}
*
* @param key {@code key}
* @return {@code value}
*/
public Object get(String key) {
return data.get(key);
}
/**
* Retrieves the {@code value} for a given {@code key} as a {@link Color}
*
* @param key {@code key}
* @return value as {@link Color}
* @throws ClassCastException if the {@code value} isn't a {@link Color}
*/
public Color getColor(String key) throws ClassCastException {
return (Color)data.get(key);
}
/**
* Retrieves the {@code value} for a given {@code key} as a {@code boolean}
*
* @param key {@code key}
* @return value as {@link Boolean}
* @throws ClassCastException if the {@code value} isn't a {@link Boolean}
*/
public boolean getBool(String key) throws ClassCastException {
return (Boolean)data.get(key);
}
/**
* Retrieves the {@code value} for a given {@code key} as a {@code double}
*
* @param key {@code key}
* @return value as {@link Double}
* @throws ClassCastException if the {@code value} isn't a {@link Double}
*/
public double getDouble(String key) throws ClassCastException {
return (Double)data.get(key);
}
/**
* Retrieves the {@code value} for a given {@code key} as an
* {@code ImageIcon}
*
* @param key {@code key}
* @return value as {@link ImageIcon}
* @throws ClassCastException if the {@code value} isn't a {@link ImageIcon}
*/
public ImageIcon getIcon(String key) throws ClassCastException {
return (ImageIcon)data.get(key);
}
/**
* Retrieves the {@code value} for a given {@code key} as an {@code int}
*
* @param key {@code key}
* @return value as {@link Integer}
* @throws ClassCastException if the {@code value} isn't a {@link Integer}
*/
public int getInt(String key) throws ClassCastException {
return (Integer)data.get(key);
}
/**
* Retrieves the {@code value} for a given {@code key} as a {@code String}
*
* @param key {@code key}
* @return value as {@link String}
* @throws ClassCastException if the {@code value} isn't a {@link String}
*/
public String getString(String key) throws ClassCastException {
return (String)data.get(key);
}
/**
* Adds a new {@link ConfigListener} to this {@code config}, this listeners
* will be notified of all the changes that happen to the <b>
* {@code non-volatile}</b> field of this config.
*
* @param listener {@link ConfigListener}
*/
public void addListener(ConfigListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
/**
* Removes a previously registered {@link ConfigListener}
*
* @param listener {@link ConfigListener}
*/
public void removeListener(ConfigListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
private void fireEvent(
final String key,
final Object oval,
final Object nval)
{
if (listeners.isEmpty()){
return;
}
if (key == null || key.isEmpty() || Objects.equals(oval, nval)){
return;
}
final int type;
if (oval == null) {
type = ConfigEvent.VALUE_ADDED;
} else if (nval == null) {
type = ConfigEvent.VALUE_REMOVED;
} else {
type = ConfigEvent.VALUE_UPDATED;
}
final ConfigEvent evt = new ConfigEvent() {
@Override public int getChangeType() {return type;}
@Override public String getChangedKey() {return key ;}
@Override public Object getOldValue () {return oval;}
@Override public Object getNewValue () {return nval;}
@Override public boolean isKey(String k) {return key.equals(k);}
};
synchronized(listeners) {
for (ConfigListener lr : listeners) {
lr.somethingChange(evt);
}
}
}
/**
* Saves all the available Configs in the given {@code OutputStream}.
* <p><i>Note:</i> all of the values of the {@code Config} object must be {@link Serializable}
* for this to work</p>
* @param os {@code OutputStream} on which to write
* @throws IOException in case an I/O error occurs
* @see Config#save(java.io.OutputStream)
* @see Config#read(java.io.InputStream, java.lang.String)
*/
public static void saveAll(OutputStream os) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(os)) {
oos.writeObject(CONFIGS);
}
}
/**
* Saves {@code this} {@code Config} in the given {@code OutputStream}.
* <p><i>Note:</i> all of the values of the {@code Config} object must be {@link Serializable}
* for this to work</p>
* @param os {@code OutputStream} on which to write
* @throws IOException in case an I/O error occurs
* @see Config#saveAll(java.io.OutputStream)
* @see Config#read(java.io.InputStream, java.lang.String)
*/
public void save(OutputStream os) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(os)) {
oos.writeObject(this);
}
}
/**
* Read a {@code Config} or {@code Map} of configs from the given {@link InputStream}.
*
* @param is {@code InputStream} from which to read
* @param name If reading a single {@code Config} this is the name it will have when calling
* {@link Config#from(java.lang.String)}, it should be {@code null} when reading a set of
* configs.
* @throws IOException in case an I/O error occurs
* @throws ClassNotFoundException If the {@code InputStream} doesn't point to an appropriate
* {@code Config}
* @see Config#save(java.io.OutputStream)
* @see Config#saveAll(java.io.OutputStream)
*/
@SuppressWarnings("unchecked")
public static void read(InputStream is, String name) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(is)) {
Object obj = ois.readObject();
if (obj instanceof Config) {
CONFIGS.put(name, (Config)obj);
} else if (obj instanceof HashMap) {
CONFIGS.putAll((HashMap<String, Config>)obj);
} else {
String msg = "This Input Stream doesn't contain a valid Config object";
throw new IllegalArgumentException(msg);
}
}
}
}