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

com.sshtools.jini.config.INISet Maven / Gradle / Ivy

package com.sshtools.jini.config;

import com.sshtools.jini.Data;
import com.sshtools.jini.INI;
import com.sshtools.jini.INI.Section;
import com.sshtools.jini.INIReader;
import com.sshtools.jini.INIWriter;
import com.sshtools.jini.Interpolation;
import com.sshtools.jini.WrappedINI;
import com.sshtools.jini.config.Monitor.MonitorHandle;
import com.sshtools.jini.schema.INISchema;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * Manages a set of INI files for configuration of a particular subsystem. Every
 * set has an "App", this determines the name of the root configuration
 * directory. Files can be scoped globally (e.g. /etc/pretty), or per-user
 * (~/.config/pretty). The path becomes the root configuration directories.
 * 

* Further, every INISet has a name. This determines the name in the apps root * configuration directories of the primary file, e.g. /etc/pretty/pretty.ini. *

* Files scope globally are read-only, but the primary user * scoped files may be written. *

* Every scoped file may have a counterpart drop-in directory, e.g. * ~/.config/jini/jini.d. All files in here are read as if they were a * section in the primary file with the same name as the file. *

* Files are in the order .. *

    *
  • Default class path configuration
  • *
  • Global scoped primary file
  • *
  • Global scoped drop-in files
  • *
  • User scoped primary file
  • *
  • User scoped drop-in files
  • *
*

* As each file is read, keys that already exist are replaced, and sections that * already exist are merged. */ public final class INISet implements Closeable { private static final String DEFAULT_APP_NAME = "jini"; private final static String os = System.getProperty("os.name", "unknown").toLowerCase(); private abstract static class AbstractWrapper extends WrappedINI.AbstractWrapper { public AbstractWrapper(DEL delegate, AbstractWrapper parent, INISet set) { super(delegate, parent, set); } @Override public Section create(String... path) { // var ref = userObject.ref(Scope.USER); // var wtrbl = ref.writable(); // var wtrblDoc = wtrbl.document(); // var fullSec = this instanceof Section ? wtrblDoc.section(path()) : wtrblDoc; // fullSec.create(path); // try { // wtrbl.write(); // } catch (IOException e) { // throw new UncheckedIOException(e); // } return super.create(path); } @Override public final Optional getAllOr(String key) { var path = path(); if(path.length > 0) { var var = System.getProperty("slf4jtty." +String.join(".", path) + "." + key); if(var != null) { return Optional.of(new String[] { var }); } } return delegate.getAllOr(key); } @Override protected SectionWrapper createWrappedSection(Section delSec) { return new SectionWrapper(delSec, this, userObject); } @Override public final > void putAllEnum(String key, E... values) { doOnWritable(key, data -> data.putAllEnum(key, values)); } @Override public final void putAll(String key, String... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final void putAll(String key, int... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final void putAll(String key, short... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final void putAll(String key, long... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final void putAll(String key, float... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final void putAll(String key, double... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final void putAll(String key, boolean... values) { doOnWritable(key, data -> data.putAll(key, values)); } @Override public final boolean remove(String key) { var res = new AtomicBoolean(); doOnWritable(key, data -> res.set(data.remove(key))); return res.get(); } private void doOnWritable(String key, Consumer task) { var ref = userObject.ref(userObject.writeScope.orElse(Scope.USER)); try { var wtrblDoc = ref.document(); if (delegate instanceof INI) { task.accept(wtrblDoc); } else { var sec = (Section) delegate; var thisSectionPath = sec.path(); var wtrblSec = wtrblDoc.obtainSection(thisSectionPath); task.accept(wtrblSec); } ref.write(); } catch (IOException ioe) { throw new UncheckedIOException(ioe); } finally { task.accept(delegate); } } } private final static class SectionWrapper extends AbstractWrapper

implements Section { public SectionWrapper(Section delegate, AbstractWrapper parent, INISet set) { super(delegate, parent, set); } @Override public final void remove() { delegate.remove(); ((AbstractWrapper) parent).removeSection(delegate); } @Override public final String key() { return delegate.key(); } @Override public final Section[] parents() { return wrapSections(delegate.parents()); } @Override public final String[] path() { return delegate.path(); } @Override public final Section parent() { if (parent instanceof Section) { return (Section) parent; } else throw new IllegalStateException("Root section."); } } private final static class RootWrapper extends AbstractWrapper implements INI { public RootWrapper(INI delegate, INISet set) { super(delegate, null, set); } @Override public INI readOnly() { return delegate.readOnly(); } @Override public INI merge(MergeMode mergeMode, INI... others) { throw new UnsupportedOperationException(); } } public enum Scope { GLOBAL, USER } public final static class Builder { private Optional writeScope = Optional.empty(); private Optional schema = Optional.empty(); private Optional defaultIni = Optional.empty(); private Optional defaultIniStream = Optional.empty(); private Optional app = Optional.empty(); private Optional monitor = Optional.empty(); private Map paths = new HashMap<>(); private List scopes = new ArrayList<>(); private boolean systemPropertyOverrides = true; private String extension = ".ini"; private boolean dropInDirectories = true; private Optional> readerFactory = Optional.empty(); private Optional> writerFactory = Optional.empty(); private final String name; public boolean closeDefaultIniStream; public Builder(String name) { this.name = name; } public Builder withReaderFactory(Supplier readerFactory) { this.readerFactory = Optional.of(readerFactory); return this; } public Builder withWriterFactory(Supplier writerFactory) { this.writerFactory = Optional.of(writerFactory); return this; } public Builder withoutDropInDirectories() { return withDropInDirectories(false); } public Builder withDropInDirectories(boolean dropInDirectories) { this.dropInDirectories = dropInDirectories; return this; } public Builder withExtension(String extension) { this.extension = extension; return this; } public Builder withoutSystemPropertyOverrides() { return withSystemPropertyOverrides(false); } public Builder withSystemPropertyOverrides(boolean systemPropertyOverrides) { this.systemPropertyOverrides = systemPropertyOverrides; return this; } public Builder withMonitor(Monitor monitor) { this.monitor = Optional.of(monitor); return this; } public Builder withWriteScope(Scope scope) { this.writeScope = Optional.of(scope); return this; } public Builder withScopes(Scope... scopes) { this.scopes = Arrays.asList(scopes); return this; } public Builder withApp(Class app) { return withApp(app.getName()); } public Builder withApp(String app) { this.app = Optional.of(app); return this; } public Builder withSchema(Class base, String resource) { try (var in = base.getResourceAsStream(resource)) { return withSchema(in); } catch (IOException ioe) { throw new UncheckedIOException(ioe); } } public Builder withDefault(Class base, String resource) { closeDefaultIniStream = true; return withDefault(base.getResourceAsStream(resource)); } public Builder withOptionalDefault(Class base, String resource) { var in = base.getResourceAsStream(resource); if(in == null) return this; return withDefault(in); } public Builder withSchema(Path path) { return withSchema(INISchema.fromFile(path)); } public Builder withSchema(InputStream in) { return withSchema(INISchema.fromInput(in)); } public Builder withSchema(INISchema schema) { this.schema = Optional.of(schema); return this; } public Builder withDefault(InputStream in) { this.defaultIniStream = Optional.of(in); return this; } public Builder withDefault(INI defaultIni) { this.defaultIni = Optional.of(defaultIni); return this; } public Builder withPath(Scope scope, Path path) { paths.put(scope, path); return this; } public INISet build() { return new INISet(this); } } private static INIReader.Builder defaultReader() { return new INIReader.Builder().withInterpolator(Interpolation.defaults()); } public final static class INIRef { private final Optional path; private final Scope scope; private INI ini; private final Optional> writerFactory; INIRef(INI doc, Optional> writerFactory) { this.scope = Scope.GLOBAL; this.ini = doc; this.path = Optional.empty(); this.writerFactory = writerFactory; } public INI document() { return ini; } INIRef(Path path, Scope scope, Optional> readerFactory, Optional> writerFactory) { this.path = Optional.of(path); this.scope = scope; this.writerFactory = writerFactory; if (Files.exists(path)) { try { ini = readerFactory.map(Supplier::get).orElseGet(INISet::defaultReader).build().read(path); } catch (IOException e) { throw new UncheckedIOException(e); } catch (ParseException e) { throw new IllegalArgumentException(e); } } else { ini = INI.create(); } } public void write() throws IOException { writerFactory.map(Supplier::get).orElseGet(this::defaultWriterBuilder).build().write(document(), path.orElseThrow(() -> new IllegalStateException("No path."))); } protected INIWriter.Builder defaultWriterBuilder() { return new INIWriter.Builder(); } public boolean isWritable() { return path.isPresent() && Files.isWritable(path.get()); } public Path path() { return path.orElseThrow(() -> new IllegalStateException("No path.")); } // Reader reader() { // if(ini.isPresent()) { // return new StringReader(ini.toString()); // } // else // return new StringReader(""); // } } private final Optional schema; private final Optional defaultIni; private List scopes = new ArrayList<>(); private final String app; private final String extension; private final Map paths; private final List refs = new ArrayList<>(); private final List handles = new ArrayList<>(); private final String name; private final Optional monitor; private final INI master; private final Optional writeScope; private final ScheduledExecutorService executor; private ScheduledFuture reloadTask; private final INI wrapper; private final boolean systemPropertyOverrides; private final boolean dropInDirectories; private final Optional> readerFactory; private final Optional> writerFactory; private INISet(Builder builder) { this.readerFactory = builder.readerFactory; this.writerFactory = builder.writerFactory; this.monitor = builder.monitor; this.dropInDirectories = builder.dropInDirectories; this.extension = builder.extension; this.systemPropertyOverrides = builder.systemPropertyOverrides; this.schema = builder.schema; if(builder.defaultIniStream.isPresent()) { var in = builder.defaultIniStream.get(); try { this.defaultIni = Optional.of(readerFactory.map(Supplier::get).orElseGet(INISet::defaultReader).build().read(in)); } catch (IOException e) { throw new UncheckedIOException(e); } catch (ParseException e) { throw new IllegalArgumentException(e); } finally { if(builder.closeDefaultIniStream) { try { in.close(); } catch (IOException e) { throw new UncheckedIOException(e); } } } } else { this.defaultIni = builder.defaultIni; }; this.app = builder.app.orElse(DEFAULT_APP_NAME); this.paths = Collections.unmodifiableMap(new HashMap<>(builder.paths)); this.name = builder.name; this.scopes = Collections.unmodifiableList(new ArrayList<>(builder.scopes)); this.executor = Executors.newSingleThreadScheduledExecutor(); this.writeScope = builder.writeScope; master = load(); if(this.schema.isPresent()) { wrapper = this.schema.get().facadeFor(new RootWrapper(master, this)); } else wrapper = new RootWrapper(master, this); } public INISchema schema() { return schema.get(); } public INIRef ref(Scope scope) { return refs.stream().filter(ref -> ref.scope.equals(scope)).findFirst() .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("No ref for scope {0}", scope))); } public INI document() { return wrapper; } public Path rootPathForScope(Scope scope) { var root = paths.get(scope); if (root == null) { if (isLinux()) { switch (scope) { case GLOBAL: return Paths.get("/etc"); case USER: return resolveHome().resolve(".config"); default: throw new UnsupportedOperationException(); } } else if (isWindows()) { switch (scope) { case GLOBAL: return Paths.get("C:\\Program Files\\Common Files"); case USER: return resolveHome().resolve("AppData").resolve("Roaming"); default: throw new UnsupportedOperationException(); } } else { switch (scope) { case GLOBAL: return Paths.get("/etc"); case USER: return resolveHome(); default: throw new UnsupportedOperationException(); } } } return root; } private static boolean isLinux() { return os.contains("linux"); } private static boolean isWindows() { return os.contains("windows"); } public Path appPathForScope(Scope scope) { var root = rootPathForScope(scope); if (!isLinux() && !isWindows() && scope == Scope.USER) { return root.resolve("." + app); } else { return root.resolve(app); } } @Override public void close() { try { cancelReloadTask(); closeMonitorHandles(); } finally { executor.shutdown(); } } private void closeMonitorHandles() { handles.forEach(MonitorHandle::close); handles.clear(); } private INI load() { /* First add the default, if any */ defaultIni.ifPresent(doc -> refs.add(new INIRef(doc, writerFactory))); if (scopes.isEmpty()) { load(Scope.GLOBAL); load(Scope.USER); } else { scopes.forEach(this::load); } return mergeToMaster(); } private INI mergeToMaster() { INI master = null; for (var ref : refs) { if (master == null) { master = ref.ini; } else { merge(master, ref.ini, true); } } if (master == null) master = INI.create(); return master; } private void load(Scope scope) { var path = appPathForScope(scope); var setRootPath = path.resolve(name + extension); var setRootDirPath = path.resolve(name + ".d"); /* * Watch for either [name].ini appearing, disappearing or changing, or [name].d * appearing / disappearing */ if (!Files.exists(path)) { try { Files.createDirectories(path); } catch (IOException ioe) { } } /* Next look for .ini as a file */ refs.add(new INIRef(setRootPath, scope, readerFactory, writerFactory)); if (Files.exists(path)) { /* Now look for .d as a directory */ if (dropInDirectories && Files.exists(setRootDirPath)) { try (var strm = Files.newDirectoryStream(setRootDirPath, f -> f.getFileName().toString().endsWith(extension))) { strm.forEach(p -> { refs.add(new INIRef(p, scope, readerFactory, writerFactory)); }); } catch (IOException e) { throw new UncheckedIOException(e); } monitor.ifPresent(mtr -> { handles.add(mtr.monitor(setRootDirPath, (ce) -> { reload(); })); }); } monitor.ifPresent(mtr -> { handles.add(mtr.monitor(path, (ce) -> { var fullContext = path.resolve(ce.context()); if (fullContext.equals(setRootPath) || fullContext.equals(setRootDirPath)) { reload(); } })); }); } } private void reload() { cancelReloadTask(); reloadTask = executor.schedule(() -> { try { refs.clear(); closeMonitorHandles(); merge(master, load(), false); } catch (Exception e) { } }, 1, TimeUnit.SECONDS); } private void merge(Data oldDoc, Data newDoc, boolean init) { mergeValues(oldDoc, newDoc, init); mergeSections(oldDoc, newDoc, init); } private String sectionName(Data newDoc) { if (newDoc instanceof Section) { var sec = (Section)newDoc; return String.join(".", sec.path()); } else return ""; } private void mergeSections(Data oldDoc, Data newDoc, boolean init) { /* TODO: multiple sections with same key */ for (var en : newDoc.sections().keySet()) { var newSec = newDoc.section(en); var oldSec = oldDoc.sectionOr(en).orElse(oldDoc.create(en)); merge(oldSec, newSec, init); } if (!init) { for (var it = oldDoc.sections().values().iterator(); it.hasNext();) { var oldSec = it.next()[0]; if (!newDoc.containsSection(oldSec.key())) { it.remove(); } } } } private void mergeValues(Data oldDoc, Data newDoc, boolean init) { for (var en : newDoc.rawValues().entrySet()) { var oldVal = oldDoc.rawValues().get(en.getKey()); var newVal = en.getValue(); if (!Arrays.equals(oldVal, newVal)) { if (!init) { if (oldVal == null) { if (newDoc instanceof Section) { var sec = (Section)newDoc; } } } oldDoc.putAll(en.getKey(), newVal); } } if (!init) { for (var key : new ArrayList<>(oldDoc.keys())) { if (!newDoc.contains(key)) { oldDoc.remove(key); } } } } protected void cancelReloadTask() { if (reloadTask != null) { reloadTask.cancel(false); } } private Path resolveHome() { return Paths.get(System.getProperty("user.home")); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy