diff --git a/README.md b/README.md
index 7698545..d9293d0 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,10 @@ And for writing an INI document ..
### 0.2.4
+ * Added `onValueUpdate()` and `onSectionUpdate()` to listen for changes to document.
+
+### 0.2.3
+
* Added `keys()` method.
* Made `containsSection(String)` now support testing for nested sections by changing the signature to `containsSection(String...)`.
diff --git a/pom.xml b/pom.xml
index d5f718c..3bdad8a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
4.0.0
com.sshtools
jini
- 0.2.3
+ 0.2.4
Simple Json Configuration
UTF-8
diff --git a/src/main/java/com/sshtools/jini/Data.java b/src/main/java/com/sshtools/jini/Data.java
index 8ea95a0..5f2ab4e 100644
--- a/src/main/java/com/sshtools/jini/Data.java
+++ b/src/main/java/com/sshtools/jini/Data.java
@@ -15,14 +15,17 @@
*/
package com.sshtools.jini;
+import java.io.Closeable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
@@ -34,6 +37,95 @@
* contained within that document {@link Section}.
*/
public interface Data {
+
+ public enum UpdateType {
+ /**
+ * A new key or section was added
+ */
+ ADD,
+ /**
+ * An existing key or section was removed
+ */
+ REMOVE,
+ /**
+ * An existing key or section was updated
+ */
+ UPDATE;
+ }
+
+ /**
+ * Carries information about a value update
+ */
+ public final static class ValueUpdateEvent {
+ private final String key;
+ private final String[] oldValues;
+ private final String[] newValues;
+
+ ValueUpdateEvent(String key, String[] oldValues, String[] newValues) {
+ super();
+ this.key = key;
+ this.oldValues = oldValues;
+ this.newValues = newValues;
+ }
+
+ public UpdateType type() {
+ if(oldValues == null && newValues != null)
+ return UpdateType.ADD;
+ else if(oldValues != null && newValues == null)
+ return UpdateType.REMOVE;
+ else
+ return UpdateType.UPDATE;
+ }
+
+ public String key() {
+ return key;
+ }
+
+ public String[] oldValues() {
+ return oldValues;
+ }
+
+ public String[] nwValues() {
+ return newValues;
+ }
+ }
+
+ /**
+ * Carries information about a section update
+ */
+ public final static class SectionUpdateEvent {
+ private final UpdateType type;
+ private final Section section;
+
+ SectionUpdateEvent(UpdateType type, Section section) {
+ this.type = type;
+ this.section = section;
+ }
+
+ public UpdateType type() {
+ return type;
+ }
+
+ public Section section() {
+ return section;
+ }
+ }
+
+ @FunctionalInterface
+ public interface Handle extends Closeable {
+ @Override
+ void close();
+ }
+
+ @FunctionalInterface
+ public interface ValueUpdate {
+ void update(ValueUpdateEvent evt);
+ }
+
+ @FunctionalInterface
+ public interface SectionUpdate {
+ void update(SectionUpdateEvent evt);
+ }
/**
* Abstract implementation of {@link Data}.
@@ -45,7 +137,10 @@ public abstract class AbstractData implements Data {
final boolean preserveOrder;
final boolean caseSensitiveKeys;
final boolean caseSensitiveSections;
- final boolean emptyValues;
+ final boolean emptyValues;
+
+ final List valueUpdate = new CopyOnWriteArrayList<>();
+ final List sectionUpdate = new CopyOnWriteArrayList<>();
AbstractData(boolean emptyValues, boolean preserveOrder, boolean caseSensitiveKeys, boolean caseSensitiveSections,
Map values, Map sections) {
@@ -65,8 +160,23 @@ public abstract class AbstractData implements Data {
}
@Override
+ public Handle onValueUpdate(ValueUpdate listener) {
+ valueUpdate.add(listener);
+ return () -> valueUpdate.remove(listener);
+ }
+
+ @Override
+ public Handle onSectionUpdate(SectionUpdate listener) {
+ sectionUpdate.add(listener);
+ return () -> sectionUpdate.remove(listener);
+ }
+
+ @Override
public boolean remove(String key) {
- return values.remove(key) != null;
+ var was = values.get(key);
+ var removed = values.remove(key) != null;
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, null)));
+ return removed;
}
@Override
@@ -101,39 +211,52 @@ public Set keys() {
@Override
public void putAll(String key, String... values) {
- this.values.put(key, nullCheck(values));
+ var was = this.values.put(key, nullCheck(values));
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, values)));
}
@Override
public void putAll(String key, int... values) {
- this.values.put(key, nullCheck(IntStream.of(values).boxed().map(i -> i.toString()).toArray((s) -> new String[s])));
+ var sval = nullCheck(IntStream.of(values).boxed().map(i -> i.toString()).toArray((s) -> new String[s]));
+ var was = this.values.put(key, sval);
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, sval)));
}
@Override
public void putAll(String key, short... values) {
- this.values.put(key, nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s])));
+ var sval = nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s]));
+ var was = this.values.put(key, sval);
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, sval)));
}
@Override
public void putAll(String key, long... values) {
- this.values.put(key, nullCheck(LongStream.of(values).boxed().map(i -> i.toString()).toArray((s) -> new String[s])));
+ var sval = nullCheck(LongStream.of(values).boxed().map(i -> i.toString()).toArray((s) -> new String[s]));
+ var was = this.values.put(key, sval);
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, sval)));
}
@Override
public void putAll(String key, float... values) {
- this.values.put(key, nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s])));
+ var sval = nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s]));
+ var was = this.values.put(key, sval);
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, sval)));
}
@Override
public void putAll(String key, double... values) {
- this.values.put(key, nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s])));
+ var sval = nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s]));
+ var was = this.values.put(key, sval);
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, sval)));
}
@Override
public void putAll(String key, boolean... values) {
- this.values.put(key, nullCheck(arrayToList(values).stream().map(i -> {
+ var sval = nullCheck(arrayToList(values).stream().map(i -> {
return i.toString();
- }).toArray((s) -> new String[s])));
+ }).toArray((s) -> new String[s]));
+ var was = this.values.put(key, sval);
+ valueUpdate.forEach(l -> l.update(new ValueUpdateEvent(key, was, sval)));
}
@Override
@@ -180,6 +303,8 @@ public Section create(String... path) {
newSection = new Section(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveKeys,
parent == null ? this : parent, name);
(parent == null ? sections : parent.sections).put(name, new Section[] { newSection });
+ var evt = new SectionUpdateEvent(UpdateType.ADD, newSection);
+ sectionUpdate.forEach(l -> l.update(evt));
} else {
if (last) {
newSection = new Section(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveKeys,
@@ -190,6 +315,8 @@ public Section create(String... path) {
} else {
newSection = existing[0];
}
+ var evt = new SectionUpdateEvent(UpdateType.UPDATE, newSection);
+ sectionUpdate.forEach(l -> l.update(evt));
}
parent = newSection;
}
@@ -209,6 +336,8 @@ void remove(Section section) {
sections.remove(section.key());
else
sections.put(section.key(), l.toArray(new Section[0]));
+ var evt = new SectionUpdateEvent(UpdateType.REMOVE, section);
+ sectionUpdate.forEach(lstnr -> lstnr.update(evt));
}
private String[] nullCheck(String... objs) {
@@ -1162,6 +1291,22 @@ default Optional getAllBooleanOr(String key) {
var arr = getAllOr(key).map(s -> Arrays.asList(s).stream().map(v -> Boolean.parseBoolean(v)).toArray());
return arr.isEmpty() ? Optional.empty() : Optional.of(toPrimitiveBooleanArray(arr.get()));
}
+
+ /**
+ * Receive notifications when a value is updated
+ *
+ * @param callback callback
+ * @return handler to stop listening
+ */
+ Handle onValueUpdate(ValueUpdate listener);
+
+ /**
+ * Receive notifications when a section is updated
+ *
+ * @param callback callback
+ * @return handler to stop listening
+ */
+ Handle onSectionUpdate(SectionUpdate listener);
private static short[] toPrimitiveShortArray(final Object[] shortList) {
final short[] primitives = new short[shortList.length];