All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sshtools.jini.Data Maven / Gradle / Ivy

The newest version!
/**
 * Copyright © 2023 JAdaptive Limited ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
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.HashMap;
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;

import com.sshtools.jini.INI.MissingVariableMode;
import com.sshtools.jini.INI.Section;
import com.sshtools.jini.INI.SectionImpl;
import com.sshtools.jini.Interpolation.Interpolator;

/**
 * Base interface shared by both the document object {@link INI} and the sections
 * 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;
		private final Optional parent;

		ValueUpdateEvent(Optional parent, String key, String[] oldValues, String[] newValues) {
			super();
			this.key = key;
			this.oldValues = oldValues;
			this.newValues = newValues;
			this.parent = parent;
		}

		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 Optional parentOr() {
			return parent;
		}
		
		public Data parent() {
			return parent.orElseThrow(() -> new IllegalStateException("Has no parent."));
		}

		public String key() {
			return key;
		}

		public String[] oldValues() {
			return oldValues;
		}

		public String[] newValues() {
			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}.
     */
    public abstract class AbstractData implements Data {

        final Map sections;
        final Map values;
        final boolean preserveOrder;
        final boolean caseSensitiveKeys;
        final boolean caseSensitiveSections;
        final boolean emptyValues;
        
        final List valueUpdate = new CopyOnWriteArrayList<>();
        final List sectionUpdate = new CopyOnWriteArrayList<>();
        final Optional interpolator;	

        final Optional variablePattern;
        final MissingVariableMode missingVariableMode;

        AbstractData(boolean emptyValues, boolean preserveOrder, boolean caseSensitiveKeys, boolean caseSensitiveSections,
                Map values, Map sections, Optional interpolator,
                Optional variablePattern, MissingVariableMode missingVariableMode) {
            super();
            this.interpolator = interpolator; 
            this.emptyValues = emptyValues;
            this.sections = sections;
            this.values = values;
            this.preserveOrder = preserveOrder;
            this.caseSensitiveKeys = caseSensitiveKeys;
            this.caseSensitiveSections = caseSensitiveSections;
            this.variablePattern = variablePattern;
            this.missingVariableMode = missingVariableMode;
        }

        AbstractData(boolean emptyValues, boolean preserveOrder, boolean caseSensitiveKeys, boolean caseSensitiveSections, Optional interpolator,
                Optional variablePattern, MissingVariableMode missingVariableMode) {
            this(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveSections,
                    INIReader.createPropertyMap(preserveOrder, caseSensitiveKeys),
                    INIReader.createSectionMap(preserveOrder, caseSensitiveSections), interpolator,
                    variablePattern, missingVariableMode);
        }

        @Override
		public boolean empty() {
			return sections.isEmpty() && values.isEmpty();
		}

		@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 void clear() {
			values.clear();
			fireUpdated(this, null, null, null);
		}

		@Override
        public boolean remove(String key) {
			var was = values.get(key);
            var removed = values.remove(key) != null;
			fireUpdated(this, key, was, null);
            return removed;
        }

		protected void fireUpdated(AbstractData sec, String key, String[] was, String[] newVals) {
			var evt = new ValueUpdateEvent(Optional.of(sec), key, was, newVals);
			valueUpdate.forEach(l -> {
				l.update(evt);
			});
		}

        @Override
        public boolean contains(String key) {
            return values.containsKey(key);
        }

        @Override
        public boolean containsSection(String... key) {
        	if(key.length == 0)
        		throw new IllegalArgumentException();
        	else if(key.length == 1)
        		return sections.containsKey(key[0]);
        	else {
        		Data section = this;
        		for(var k : key) {
        			if(section.sections().containsKey(k)) {
        				section = section.sections().get(k)[0];
        			}
        			else {
        				return false; 
        			}
        		}
        		return true;
        	}
        }

        @Override
    	public int size() {
    		return values.size();
    	}
    	
        @Override
		public Set keys() {
			return values.keySet();
		}

		@Override
        public void putAll(String key, String... values) {
            var was = this.values.put(key, nullCheck(values));
			fireUpdated(this, key, was, values);
        }

        @Override
        public void putAll(String key, int... values) {
            var sval = nullCheck(IntStream.of(values).boxed().map(i -> i.toString()).toArray((s) -> new String[s]));
			var was = this.values.put(key, sval);
			fireUpdated(this, key, was, sval);
        }

        @Override
        public void putAll(String key, short... values) {
            var sval = nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s]));
			var was = this.values.put(key, sval);
			fireUpdated(this, key, was, sval);
        }

        @Override
        public void putAll(String key, long... values) {
            var sval = nullCheck(LongStream.of(values).boxed().map(i -> i.toString()).toArray((s) -> new String[s]));
			var was = this.values.put(key, sval);
			fireUpdated(this, key, was, sval);
        }

        @Override
        public void putAll(String key, float... values) {
            var sval = nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s]));
			var was = this.values.put(key, sval);
			fireUpdated(this, key, was, sval);
        }

        @Override
        public void putAll(String key, double... values) {
            var sval = nullCheck(arrayToList(values).stream().map(i -> i.toString()).toArray((s) -> new String[s]));
			var was = this.values.put(key, sval);
			fireUpdated(this, key, was, sval);
        }

        @Override
        public void putAll(String key, boolean... values) {
            var sval = nullCheck(arrayToList(values).stream().map(i -> {
                return i.toString();
            }).toArray((s) -> new String[s]));
			var was = this.values.put(key, sval);
			fireUpdated(this, key, was, sval);
        }

        @SuppressWarnings("unchecked")
		@Override
		public > void putAllEnum(String key, E... values) {
        	 var sval = nullCheck(Arrays.asList(values).stream().map(i -> {
                 return i.toString();
             }).toArray((s) -> new String[s]));
 			var was = this.values.put(key, sval);
 			fireUpdated(this, key, was, sval);
		}

		@Override
        public final Map values() {
        	if(interpolator.isPresent()) {
        		var m = new HashMap();
        		for(var en : rawValues().entrySet()) {
        			m.put(en.getKey(), interpolate(en.getValue()));
        		}
        		return Collections.unmodifiableMap(m);
        	}
        	else
        		return rawValues();
        }

		@Override
        public Map rawValues() {
       		return values;
        }

        @Override
        public Map sections() {
            return sections;
        }

        @Override
        public Optional allSectionsOr(String... path) {
        	if(path.length == 0) {
        		return Optional.of(sections().values().stream().flatMap(sections -> Arrays.asList(sections).stream()).collect(Collectors.toList()).toArray(new Section[0]));
        	}
            Data current = this;
            Section[] sections = null;
            for(var key : path) {
                sections = current.sections().get(key);
                if(sections == null)
                    return Optional.empty();
                else
                    current = sections[0];
            }
            return Optional.ofNullable(sections);
        }

        @Override
        public Optional getAllOr(String key) {
        	if(interpolator.isPresent())
        		return Optional.ofNullable(values.get(key)).map(this::interpolate);
        	else
        		return Optional.ofNullable(values.get(key));
        }

        @Override
        public Section create(String... path) {
            Section newSection = null;
            Section parent = this instanceof Section ? (Section) this : null;
            for (int i = 0; i < path.length; i++) {
                var last = i == path.length - 1;
                var name = path[i];
                var existing = parent == null ? sections.get(name) : parent.sections().get(name);
                if (existing == null) {
                    newSection = new SectionImpl(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveKeys,
                            parent == null ? this : parent, name, interpolator, variablePattern, missingVariableMode);
                    (parent == null ? sections : parent.sections()).put(name, new Section[] { newSection });
					fireSectionUpdate(this, newSection, UpdateType.ADD);
                } else {
                    if (last) {
                        newSection = new SectionImpl(emptyValues, preserveOrder, caseSensitiveKeys, caseSensitiveKeys,
                                parent == null ? this : parent, name, interpolator, variablePattern, missingVariableMode);
                        var newSections = new Section[existing.length + 1];
                        System.arraycopy(existing, 0, newSections, 0, existing.length);
                        newSections[existing.length] = newSection;
                    } else {
                        newSection = existing[0];
                    }
					fireSectionUpdate(this, newSection, UpdateType.UPDATE);
                }
                parent = newSection;
            }
            if (newSection == null)
                throw new IllegalArgumentException("No section path");
            return newSection;
        }

        @Override
        public String asString() {
            return new INIWriter.Builder().build().write(this);
        }

		protected void fireSectionUpdate(AbstractData parent, Section newSection, UpdateType type) {
			var evt = new SectionUpdateEvent(type, newSection);
			sectionUpdate.forEach(l -> l.update(evt));
		}
        
        void remove(Section section) {
            var v = sections.get(section.key());
            if (v == null) {
                throw new IllegalArgumentException("Section not part of this section");
            }
            var l = new ArrayList<>(Arrays.asList(v));
            l.remove(section);
            if (l.isEmpty())
                sections.remove(section.key());
            else
                sections.put(section.key(), l.toArray(new Section[0]));
            fireSectionUpdate(this, section, UpdateType.REMOVE);
        }
        
        private String[] interpolate(String[] vals) {
        	for(int i = 0 ; i < vals.length; i++) {
        		vals[i] = Interpolation.str(this, 
        				variablePattern.orElse(Interpolation.DEFAULT_VARIABLE_PATTERN), 
        				vals[i],
        				Interpolation.compound(
        					interpolator.get(),
        					(data, var) -> {
        						switch(missingVariableMode) {
        						case BLANK:
        							return "";
        						case SKIP:
        							return null;
        						default:
        							throw new IllegalArgumentException(MessageFormat.format("Unknown string variable ''{0}'''", var));
        						}
        					})
        				);
        	}
        	return vals;
        }
        
        private String[] nullCheck(String... objs) {
            if(objs == null) {
                if(emptyValues)
                    return new String[0];
                else
                    throw new IllegalArgumentException("Value may not be null.");
            }
            else  {
                if((objs.length == 1 && objs[0] == null) || (objs.length == 0)) {
                    if(emptyValues)
                        return new String[0];
                    else
                        throw new IllegalArgumentException("Value may not be null.");
                }
                else {
                    for(var obj : objs)
                        if(obj == null)
                            throw new IllegalArgumentException("Only a single value may contain null values.");
                }
            }
            return objs;
        }
    }

    /**
     * Get a  set of the underlying keys.
     * 
     * @return set of keys in this section or document
     */
    Set keys();

    String asString();

	Optional
parentOr(); default String[] path() { return new String[0]; } INI document(); int size(); void clear(); boolean empty(); /** * Get the map of the underlying values. The returned array of values * will never be null, but may potentially be an empty array. If string interpolation * is enabled, values will be processed. For unprocessed values, see {@link #rawValues()}. * * @return map of values in this section or document */ Map values(); /** * Get the map of the underlying values. The returned array of values * will never be null, but may potentially be an empty array. The values returned will * be unprocessed, i.e. if string interpolation is enabled, no strings will be replaced. * * @return map of values in this section or document */ Map rawValues(); /** * Get a map of the underlying sections. The returned array of values * will never be null, and will never be an empty array. * * @return map of sections in this section or document */ Map sections(); /** * Get a {@link Section} given it's its path relative to this document or parent * section. If there are no sections with such a path, an {@link IllegalArgumentException} will * be thrown. If there are more than one sections with such a path, the * first section will be returned. * * @param path path to section * @return optional section */ default Section section(String... path) { return sectionOr(path) .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("No section with path {0}", String.join(".", path)))); } /** * Obtain a {@link Section} given it's its path relative to this document or parent * section. Any sections missing in the path will be created. If there are more * than one sections with such a path, the first section will be returned. * * @param path path to section * @return optional section */ default Section obtainSection(String... path) { var data = this; for(var p : path) { if(data.containsSection(p)) { data = data.section(p); } else { data = data.create(p); } } return (Section)data; } /** * Get whether this document or section contains a value (empty or otherwise). * * @param key key of value * @return document or section contains key */ boolean contains(String key); /** * Remove a value give it's key. * * @param key key of value * @return value was removed */ boolean remove(String key); /** * Get whether this document or section contains a child section. Nested sections * may be specified by provided each path element. * * @param key key of section * @return document or section contains section */ boolean containsSection(String... key); /** * Put a string value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, String value) { putAll(key, value); } /** * Put an integer value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, int value) { putAll(key, value); } /** * Put a short value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, short value) { putAll(key, value); } /** * Put a long value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, long value) { putAll(key, value); } /** * Put a float value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, float value) { putAll(key, value); } /** * Put a double value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, double value) { putAll(key, value); } /** * Put a boolean value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default void put(String key, boolean value) { putAll(key, value); } /** * Put a boolean value into this document or section with the given key. Any existing * values with the same key will be entirely replaced. * * @param key key to store value under * @param value value to store */ default > void putEnum(String key, E value) { put(key, value.name()); } /** * Put zero or more {@link Enum} values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ @SuppressWarnings("unchecked") > void putAllEnum(String key, E... values); /** * Put zero or more string values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, String... values); /** * Put zero or more integer values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, int... values); /** * Put zero or more short values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, short... values); /** * Put zero or more long values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, long... values); /** * Put zero or more float values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, float... values); /** * Put zero or more double values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, double... values); /** * Put zero or more boolean values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty array of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ void putAll(String key, boolean... values); /** * Put one or more string values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void put(String key, Collection values) { putAll(key, values.toArray(new String[0])); } /** * Put one or more integer values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void putInt(String key, Collection values) { putAll(key, values.stream().mapToInt(i -> i).toArray()); } /** * Put one or more short values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void putShort(String key, Collection values) { var result = new short[values.size()]; var i = 0; for (var f : values) { result[i++] = f; } putAll(key, result); } /** * Put one or more long values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void putLong(String key, Collection values) { putAll(key, values.stream().mapToLong(i -> i).toArray()); } /** * Put one or more float values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void putFloat(String key, Collection values) { var result = new float[values.size()]; var i = 0; for (var f : values) { result[i++] = f; } putAll(key, result); } /** * Put one or more double values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void putDouble(String key, Collection values) { putAll(key, values.stream().mapToDouble(i -> i).toArray()); } /** * Put one or more boolean values into this document or section with the given key. Any existing * values with the same key will be entirely replaced. *

* If an empty list of values is supplied, an empty value will be inserted. This will * always be output by an {@link INIWriter}, but for an {@link INIReader} to be able * to correctly read this, {@link INIReader.Builder#withEmptyValues(boolean)} * must be true. * * @param key key to store value under * @param values values to store */ default void putBoolean(String key, Collection values) { var result = new boolean[values.size()]; var i = 0; for (var f : values) { result[i++] = f; } putAll(key, result); } /** * Get a {@link Section} given it's its path relative to this document or parent * section. If there are no sections with such a path, {@link Optional#isEmpty()} will * return true. If there are more than one sections with such a path, the * first section will be returned. * * @param path path to section * @return optional section */ default Optional

sectionOr(String... path) { var all = allSectionsOr(path); if (all.isEmpty()) return Optional.empty(); else { return Optional.of(all.get()[0]); } } /** * Get all sections with the given path that is relative to this document or parent. If * there are no sections with such a path, an {@link IllegalArgumentException} will be thrown. * An empty array will never be returned. *

* If no section paths are provided, all sections in this document or parent are returned. * * @param path path to section * @return sections with path */ default Section[] allSections(String... path) { return allSectionsOr(path) .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("No section with path {0}", String.join(".", path)))); } /** * Get all sections with the given path that is relative to this document or parent. * If there are no sections with such a path, {@link Optional#isEmpty()} will * return true. An empty array will never be returned. *

* If no section paths are provided, all sections in this document or parent are returned. * * @param path path to section * @return optional sections with path */ Optional allSectionsOr(String... path); /** * Create either a single section inside this document or parent section, or create * nested sections starting inside this document or parent section. *

* When creating a path of sections, if specified parent sections already exist they * will not be overwritten. *

* The final element of the path, if such a section with the same key already exists, * a 2nd section with the same key will be created. * * @param path * @return the new section */ Section create(String... path); /** * Get a single string value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default String get(String key) { return getOr(key) .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("No property with key {0}", key))); } /** * Get a single string value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default String get(String key, String defaultValue) { return getOr(key).orElse(defaultValue); } /** * Get a single string value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getOr(String key) { var all = getAllOr(key); if (all.isEmpty()) return Optional.empty(); else { var a = all.get(); if (a.length == 0) return Optional.empty(); else return Optional.of(a[0]); } } /** * Get all string values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default String[] getAll(String key) { return getAllOr(key) .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("No property with key {0}", key))); } /** * Get all string values given a key or a default value if no such key exists. * The returned array may potentially be empty, but never null. * * @param key key * @param defaultValues default values * @return values */ default String[] getAllElse(String key, String... defaultValues) { return getAllOr(key).orElse(defaultValues); } /** * Get all string values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ Optional getAllOr(String key); /** * Get a single double value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default double getDouble(String key) { return Double.parseDouble(get(key)); } /** * Get a single double value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default double getDouble(String key, double defaultValue) { return getOr(key).map(i -> Double.parseDouble(i)).orElse(defaultValue); } /** * Get a single double value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getDoubleOr(String key) { return getOr(key).map(i -> Double.parseDouble(i)); } /** * Get all double values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default double[] getAllDouble(String key) { return Arrays.asList(getAll(key)).stream().mapToDouble(v -> Double.parseDouble(v)).toArray(); } /** * Get all double values given a key or a default value if no such key exists. * The returned array may potentially be empty, but never null. * * @param key key * @param defaultValues default values * @return values */ default double[] getAllDoubleElse(String key, double... defaultValues) { return getAllDoubleOr(key).orElse(defaultValues); } /** * Get all double values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default Optional getAllDoubleOr(String key) { return getAllOr(key).map(s -> Arrays.asList(s).stream().mapToDouble(v -> Double.parseDouble(v)).toArray()); } /** * Get a long value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default long getLong(String key) { return Long.parseLong(get(key)); } /** * Get a single long value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default long getLong(String key, long defaultValue) { return getOr(key).map(i -> Long.parseLong(i)).orElse(defaultValue); } /** * Get a single long value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getLongOr(String key) { return getOr(key).map(i -> Long.parseLong(i)); } /** * Get all long values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default long[] getAllLong(String key) { return Arrays.asList(getAll(key)).stream().mapToLong(v -> Long.parseLong(v)).toArray(); } /** * Get all long values given a key or a default value if no such key exists. * The returned array may potentially be empty, but never null. * * @param key key * @param defaultValues default values * @return values */ default long[] getAllLongElse(String key, long... defaultValues) { return getAllLongOr(key).orElse(defaultValues); } /** * Get all long values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default Optional getAllLongOr(String key) { return getAllOr(key).map(s -> Arrays.asList(s).stream().mapToLong(v -> Long.parseLong(v)).toArray()); } /** * Get an integer value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default int getInt(String key) { return Integer.parseInt(get(key)); } /** * Get a single integer value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default int getInt(String key, int defaultValue) { return getOr(key).map(i -> Integer.parseInt(i)).orElse(defaultValue); } /** * Get a single integer value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getIntOr(String key) { return getOr(key).map(i -> Integer.parseInt(i)); } /** * Get all integer values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default int[] getAllInt(String key) { return Arrays.asList(getAll(key)).stream().mapToInt(v -> Integer.parseInt(v)).toArray(); } /** * Get all integer values given a key or a default value if no such key exists. * The returned array may potentially be empty, but never null. * * @param key key * @param defaultValues default values * @return values */ default int[] getAllIntElse(String key, int... defaultValues) { return getAllIntOr(key).orElse(defaultValues); } /** * Get all integer values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default Optional getAllIntOr(String key) { return getAllOr(key).map(s -> Arrays.asList(s).stream().mapToInt(v -> Integer.parseInt(v)).toArray()); } /** * Get a short value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default short getShort(String key) { return Short.parseShort(get(key)); } /** * Get a single short value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default short getShort(String key, short defaultValue) { return getOr(key).map(i -> Short.parseShort(i)).orElse(defaultValue); } /** * Get a single short value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getShortOr(String key) { return getOr(key).map(i -> Short.parseShort(i)); } /** * Get all short values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default short[] getAllShort(String key) { return toPrimitiveShortArray(Arrays.asList(getAll(key)).stream().map(v -> Short.parseShort(v)).toArray()); } /** * Get all short values given a key or a default value if no such key exists. * The returned array may potentially be empty, but never null. * * @param key key * @param defaultValues default values * @return values */ default short[] getAllShortElse(String key, short... defaultValues) { return getAllShortOr(key).orElse(defaultValues); } /** * Get all short values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default Optional getAllShortOr(String key) { var arr = getAllOr(key).map(s -> Arrays.asList(s).stream().map(v -> Short.parseShort(v)).toArray()); return arr.isEmpty() ? Optional.empty() : Optional.of(toPrimitiveShortArray(arr.get())); } /** * Get a float value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default float getFloat(String key) { return Float.parseFloat(get(key)); } /** * Get a single float value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default float getFloat(String key, float defaultValue) { return getOr(key).map(i -> Float.parseFloat(i)).orElse(defaultValue); } /** * Get a single float value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getFloatOr(String key) { return getOr(key).map(i -> Float.parseFloat(i)); } /** * Get all float values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default float[] getAllFloat(String key) { return toPrimitiveFloatArray(Arrays.asList(getAll(key)).stream().map(v -> Float.parseFloat(v)).toArray()); } /** * Get all float values given a key or a default value if no such key exists. * The returned array may potentially be empty, but never null. * * @param key key * @param defaultValues default values * @return values */ default float[] getAllFloatElse(String key, float... defaultValues) { return getAllFloatOr(key).orElse(defaultValues); } /** * Get all float values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default Optional getAllFloatOr(String key) { var arr = getAllOr(key).map(s -> Arrays.asList(s).stream().map(v -> Float.parseFloat(v)).toArray()); return arr.isEmpty() ? Optional.empty() : Optional.of(toPrimitiveFloatArray(arr.get())); } /** * Get a boolean value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param key key * @return value */ default boolean getBoolean(String key) { return Boolean.parseBoolean(get(key)); } /** * Get a single boolean value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param key key * @param defaultValue default value * @return value or default */ default boolean getBoolean(String key, boolean defaultValue) { return getOr(key).map(i -> Boolean.parseBoolean(i)).orElse(defaultValue); } /** * Get a single boolean value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default Optional getBooleanOr(String key) { return getOr(key).map(i -> Boolean.parseBoolean(i)); } /** * Get all boolean values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ default boolean[] getAllBoolean(String key) { return toPrimitiveBooleanArray(Arrays.asList(getAll(key)).stream().map(v -> Boolean.parseBoolean(v)).toArray()); } /** * Get all boolean values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default boolean[] getAllBooleanElse(String key, boolean... defaultValues) { return getAllBooleanOr(key).orElse(defaultValues); } /** * Get all boolean values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ 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())); } /** * Get an {@link Enum} value given it's key. If the key does not exist, * an {@link IllegalArgumentException} will be thrown. If there are multiple * values for a key, the first value will be returned. * * @param type type * @param key key * @return value */ default > E getEnum(Class type, String key) { return Enum.valueOf(type, get(key)); } /** * Get a single {@link Enum} value given it's key, or a default value if does not exist. * If there are multiple values for a key, the first value will be returned. * * @param type type * @param key key * @param defaultValue default value * @return value or default */ default > E getEnum(Class type, String key, E defaultValue) { return getEnumOr(type, key).orElse(defaultValue); } /** * Get a single {@link Enum} value given it's key. If no such key exists, {@link Optional#isEmpty()} * will return true. If there are multiple values for a key, the first value will be returned. * * @param key key * @return optional value */ default > Optional getEnumOr(Class type, String key) { return getOr(key).map(i -> Enum.valueOf(type, i)); } /** * Get all {@link Enum} values given a key. If no such key exists, an * {@link IllegalArgumentException} will be thrown. The returned array * may potentially be empty, but never null. * * @param key key * @return values */ @SuppressWarnings("unchecked") default > E[] getAllEnum(Class type, String key) { return (E[]) Arrays.asList(getAll(key)).stream().map(v -> Enum.valueOf(type, v)).collect(Collectors.toList()).toArray(); } /** * Get all {@link Enum} values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ default > E[] getAllEnumElse(Class type, String key, @SuppressWarnings("unchecked") E... defaultValues) { return getAllEnumOr(type, key).orElse((E[])defaultValues); } /** * Get all {@link Enum} values given a key. If no such key exists, {@link Optional#isEmpty()} * will return true. The returned array may potentially be empty. * * @param key key * @return optional value */ @SuppressWarnings("unchecked") default > Optional getAllEnumOr(Class type, String key) { var arr = getAllOr(key).map(s -> Arrays.asList(s).stream().map(v -> Enum.valueOf(type, v)).toArray()); return arr.isEmpty() ? Optional.empty() : Optional.of((E[])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]; int index = 0; for (Object object : shortList) { primitives[index++] = ((Short) object).shortValue(); } return primitives; } private static boolean[] toPrimitiveBooleanArray(final Object[] booleanList) { final boolean[] primitives = new boolean[booleanList.length]; int index = 0; for (Object object : booleanList) { primitives[index++] = object == Boolean.TRUE ? true : Boolean.FALSE; } return primitives; } private static float[] toPrimitiveFloatArray(final Object[] floatList) { final float[] primitives = new float[floatList.length]; int index = 0; for (Object object : floatList) { primitives[index++] = ((Float) object).floatValue(); } return primitives; } private static Collection arrayToList(float[] values) { var l = new ArrayList(values.length); for(var v : values) l.add(v); return l; } private static Collection arrayToList(double[] values) { var l = new ArrayList(values.length); for(var v : values) l.add(v); return l; } private static Collection arrayToList(short[] values) { var l = new ArrayList(values.length); for(var v : values) l.add(v); return l; } public static Collection arrayToList(boolean[] values) { var l = new ArrayList(values.length); for(var v : values) l.add(v); return l; } /** * A read-only facade to this section. */ Data readOnly(); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy