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

nstream.adapter.common.AdapterUtils Maven / Gradle / Ivy

There is a newer version: 4.15.23
Show newest version
// Copyright 2015-2024 Nstream, inc.
//
// Licensed under the Redis Source Available License 2.0 (RSALv2) Agreement;
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://redis.com/legal/rsalv2-agreement/
//
// 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 nstream.adapter.common;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Optional;
import java.util.Properties;
import java.util.function.BiConsumer;
import java.util.function.Function;
import nstream.adapter.common.ingress.AssemblerFactory;
import nstream.adapter.common.ingress.AssemblyException;
import nstream.adapter.common.ingress.ContentAssembler;
import nstream.adapter.common.ingress.ValueAssembler;
import nstream.adapter.common.ingress.XmlContentAssembler;
import nstream.adapter.common.relay.DslInterpret;
import nstream.adapter.common.schedule.DeferrableException;
import swim.api.agent.AgentContext;
import swim.codec.Utf8;
import swim.json.Json;
import swim.recon.Recon;
import swim.structure.Attr;
import swim.structure.Item;
import swim.structure.Record;
import swim.structure.Slot;
import swim.structure.Text;
import swim.structure.Value;
import swim.util.Log;

/**
 * A collection of convenience methods that will commonly be invoked within
 * Nstream adapter implementations.
 */
public final class AdapterUtils {

  private AdapterUtils() {
  }

  // ===========================================================================
  // File loading utilities
  // ===========================================================================

  /**
   * Returns an {@code InputStream} for reading a system file, falling back to a
   * Java resource if required.
   *
   * @param diskPath  the path of the system file to load
   * @param clazz  the class associated with the fallback resource
   * @param resourcePath  the path of the fallback resource
   *
   * @return  an {@code InputStream} for reading the file or its fallback;
   * {@code null} if neither one could be accessed
   */
  public static InputStream openFileAsStream(String diskPath,
                                             Class clazz, String resourcePath) {
    if (diskPath == null || diskPath.isEmpty()) {
      return openResourceAsStream(resourcePath, clazz);
    }
    try {
      return new FileInputStream(diskPath);
    } catch (IOException e) {
      return openResourceAsStream(resourcePath, clazz);
    }
  }

  /**
   * Returns an {@code InputStream} for reading a system file, falling back to a
   * Java resource if required.
   *
   * @param path  the path of either a system file or fallback resource to load
   * @param clazz  the class associated with the fallback resource
   *
   * @return  an {@code InputStream} for reading the file or its fallback;
   * {@code null} if neither one could be accessed
   */
  public static InputStream openFileAsStream(String path, Class clazz) {
    return openFileAsStream(path, clazz, path);
  }

  /**
   * Returns an {@code InputStream} for reading a resource.
   *
   * @param path  the path of the resource to be read
   * @param clazz  the class whose classloader (first) or class will load the
   *               specified resource
   *
   * @return  an {@code InputStream} for reading the resource, or {@code null}
   * if it could not be accessed
   */
  public static InputStream openResourceAsStream(String path, Class clazz) {
    final InputStream result = clazz.getClassLoader().getResourceAsStream(path);
    return result == null ? clazz.getResourceAsStream(path) : result;
  }

  // ===========================================================================
  // Ingress utility functions
  // ===========================================================================

  private static final AssemblerFactory ASSEMBLER_FACTORY = new AssemblerFactory();

  static {
    ASSEMBLER_FACTORY.registerContentAssembler(new XmlContentAssembler(Value.extant()));
  }

  /**
   * Translates a {@code InputStream} out of an input that was encoded
   * or compressed with a known encoding.
   * 

Specifically, calling {@link InputStream#read()} against the returned * {@code InputStream} must return a byte of UTF-8 encoded, uncompressed data. * Be aware that the original input may be modified as a side effect. * * @param encoded the {@code InputStream} to be decoded * @param minimalContentEncoding an identifier of the encoding scheme used by * {@code encoded} * @return an {@code InputStream} whose {@code read} method(s) are UTF-8 * compatible * @throws IOException if there was an issue collecting {@code encoded} into * a UTF-8 compatible {@code InputStream} */ // TODO: enum contentEncoding argument // TODO: throw RuntimeException for bad encodings? public static InputStream decodeStream(InputStream encoded, String minimalContentEncoding) throws IOException { return ASSEMBLER_FACTORY.decodeStream(encoded, minimalContentEncoding); } /** * Parses a textual UTF-8 {@code InputStream} with a known data format into * Swim's structured data model. * * @param stream the {@code InputStream} holding the data of interest * @param minimalContentType the data format * * @return a {@code Value} representing the data in {@code stream} * * @throws IOException if there was an issue reading from {@code stream} */ public static Value assembleContent(InputStream stream, String minimalContentType) throws AssemblyException, IOException { return ASSEMBLER_FACTORY.assemble(stream, minimalContentType); } /** * Parses a {@code String} with a known data format into Swim's structured * data model. * * @param msg the data of interest * @param minimalContentType the data format * * @return a {@code Value} representing the data in {@code msg} */ public static Value assembleContent(String msg, String minimalContentType) throws AssemblyException { return ASSEMBLER_FACTORY.assemble(msg, minimalContentType); } /** * Parses an {@code Object} that is assumed to hold formatted textual data * into Swim's structured data model. * * @param content an object holding the data of interest * @param minimalContentType the data format * * @return a {@code Value} representing the data in {@code content} * * @throws IllegalArgumentException if the type of {@code content} is * unrecognized for translation */ public static Value assembleContent(Object content, String minimalContentType) throws AssemblyException { if (minimalContentType == null) { return Value.fromObject(content); // last resort } return ASSEMBLER_FACTORY.assembleContent(content, minimalContentType); } /** * Parses an {@code Object} that is assumed to hold formatted textual data * into Swim's structured data model. * * @param content an object holding the data of interest * @param assembler the parsing logic to use, falling back to {@code * swim.structure.Value#fromObject} * * @return a {@code Value} representing the data in {@code content} * * @throws IllegalArgumentException if the type of {@code content} is * unrecognized for translation */ public static Value assembleContent(Object content, ContentAssembler assembler) throws AssemblyException { if (assembler == null) { return Value.fromObject(content); // last resort } return AssemblerFactory.assembleContent(content, assembler); } /** * Parses an {@code Object} that is assumed to hold possibly but not * necessarily formatted textual data into Swim's structured data model. * * @param content an object holding the data of interest * @param assembler the parsing logic to use, falling back to {@code * swim.structure.Value#fromObject} * * @return a {@code Value} representing the data in {@code content} * * @throws IllegalArgumentException if the type of {@code content} is * unrecognized for translation */ @SuppressWarnings("unchecked") public static Value assemble(Object content, ValueAssembler assembler) throws AssemblyException { if (assembler == null) { return Value.fromObject(content); // last resort } else if (assembler instanceof ContentAssembler) { return assembleContent(content, (ContentAssembler) assembler); } else { return assembler.assemble((V) content); } } /** * Executes logic from a specified {@link AgentContext} against some input. * * @param schema the DSL-interpretable logic to run * @param agentContext the {@code AgentContext} from which to run the logic * @param scope the input value for the logic * * @throws DeferrableException if there was any issue interpreting {@code * scope} against {@code schema} */ public static void ingressDslRelay(Value schema, AgentContext agentContext, Value scope) throws DeferrableException { if (schema == null || !schema.isDistinct() || !(schema instanceof Record)) { throw new DeferrableException(schema + " is not DSL-interpretable"); } else { try { DslInterpret.interpret(agentContext, (Record) schema, scope); } catch (Exception e) { throw new DeferrableException(schema + " is not DSL-interpretable against scope " + scope, e); } } } // =========================================================================== // Egress utility functions // =========================================================================== public static String messageDismantle(Value value, String minimalContentType) { if (minimalContentType == null) { minimalContentType = ""; } switch (minimalContentType) { case "xml": throw new UnsupportedOperationException("Xml not yet implemented"); case "recon": return Recon.toString(value); case "json": return Json.toString(value); default: return value.stringValue(); } } // =========================================================================== // @config-related utilities // =========================================================================== public static void loadPropsFromUse(Log log, String name, Properties properties, Value useDef) { loadFromUse(log, name, properties, useDef, (s, p) -> loadPropsFromFile(log, name, p, s)); } private static void loadPropsFromFile(Log log, String name, Properties properties, String fname) { final Properties temp = new Properties(); try (InputStream is = openFileAsStream(fname, AdapterUtils.class)) { temp.load(is); } catch (Exception e) { log.warn("Failed to populate " + name + " from file " + fname + ": " + e.getMessage()); try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { e.printStackTrace(pw); log.warn(sw.toString()); } catch (IOException ioe) { // swallow } } for (String prop : temp.stringPropertyNames()) { if (properties.getProperty(prop) != null) { log.debug("Ignored already-defined property " + prop + " when loading from " + fname); } else { properties.setProperty(prop, temp.getProperty(prop)); } } } public static void loadPropsFromDef(Log log, String name, Properties properties, Value defDef) { if (defDef instanceof Record) { for (Item i : defDef) { if (i instanceof Slot) { if (!(i.key() instanceof Text)) { log.warn("Ignored non-Text Slot key " + i.key() + " when loading " + name + " from def"); continue; } final String key = i.key().stringValue(); if (properties.getProperty(key) != null) { log.debug("Ignored already-defined property " + key + " when loading " + name + " from def"); continue; } if (isConfigFieldMold(i.toValue())) { evaluateConfigProp(log, name, i.toValue()) .ifPresentOrElse( s -> properties.setProperty(key, s), () -> log.warn("Ignored property " + key + " that had no definition at runtime via " + i.toValue() + " when loading " + name + " from def")); } else if (!i.toValue().isDistinct() || i.toValue() instanceof Record) { log.warn("Ignored non-string Slot value " + i.toValue() + " under " + key + " when loading " + name + " from def"); } else { properties.setProperty(key, i.toValue().stringValue()); } } else { log.warn("Ignored non-Slot item " + i + " when loading " + name + " from def"); } } } else if (defDef.isDistinct()) { log.warn("Ignored non-Record def " + defDef + " when loading " + name + " from def"); } } public static S settingsFromConf(Log log, String name, String tag, Value conf, Function mold, Function cast) { if (!(conf instanceof Record)) { log.warn("Ignored invalid " + name + " configuration " + conf); return null; } if (conf.head() instanceof Attr && tag.equals(conf.head().key().stringValue(null))) { final Value use = conf.get("use"), def = conf.get("def"); if (use.isDistinct() || def.isDistinct()) { final Value evaluated = AdapterUtils.loadConf(log, tag, tag, conf, mold); return cast.apply(evaluated); } else { // legacy return cast.apply(conf); } } else { log.warn("Ignored invalid " + name + " configuration " + conf); return null; } } public static Record loadConf(Log log, String name, Value conf, Function mold) { final Record result = Record.create(); loadStructureFromUse(log, name, result, conf.get("use"), mold); loadStructureFromDef(log, name, result, conf.get("def"), mold); return result; } public static Record loadConf(Log log, String name, String tag, Value conf, Function mold) { final Record result = Record.create().attr(tag); loadStructureFromUse(log, name, result, conf.get("use"), mold); loadStructureFromDef(log, name, result, conf.get("def"), mold); return result; } static void loadStructureFromUse(Log log, String name, Record structure, Value useDef, Function mold) { loadFromUse(log, name, structure, useDef, (s, v) -> loadStructureFromFile(log, name, v, s, mold)); } // TODO: more trace/debug/info-level logging private static void loadStructureFromFile(Log log, String name, Record structure, String fname, Function mold) { try (InputStream is = openFileAsStream(fname, AdapterUtils.class)) { if (fname.endsWith(".recon")) { loadStructureFromRecord(log, name, fname, structure, (Record) Utf8.read(is, Recon.structureParser().blockParser())); } else if (fname.endsWith(".properties")) { final Properties toAdd = new Properties(); toAdd.load(is); loadStructureFromProperties(log, name, fname, structure, toAdd, mold); } else { log.warn("Ignored file " + fname + " with invalid extension"); } } catch (Exception e) { try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { e.printStackTrace(pw); log.warn(sw.toString()); } catch (IOException ioe) { // swallow } } } // TODO: logging private static void loadStructureFromProperties(Log log, String name, String fname, Record structure, Properties input, Function moldFromProperties) { final Record toAdd = moldFromProperties.apply(input); for (Item i : toAdd) { if (i instanceof Slot) { structure.updatedSlot(i.key(), i.toValue()); } } } private static void loadStructureFromRecord(Log log, String name, String fname, Record structure, Record input) { for (Item i : input) { if (!(i instanceof Slot)) { log.warn("Ignored non-Slot Item " + i + " when loading " + name + " from " + fname); } else if (structure.get(i.key()).isDistinct()) { log.debug("Ignored already-defined key " + i.key().stringValue(null) + " when loading " + name + " from " + fname); } else { structure.slot(i.key(), i.toValue()); } } } private static void loadFromUse(Log log, String name, V accumulator, Value useDef, BiConsumer load) { if (useDef instanceof Text) { load.accept(useDef.stringValue(), accumulator); } else if (useDef instanceof Record) { if (isConfigFieldMold(useDef)) { evaluateConfigProp(log, name, useDef) .ifPresentOrElse( s -> load.accept(s, accumulator), () -> log.warn("Failed to coerce " + useDef + " into a filename when evaluating " + name)); } else { for (Item i : useDef) { if (i instanceof Text) { load.accept(i.stringValue(), accumulator); } else if (isConfigFieldMold(i.toValue())) { evaluateConfigProp(log, name, i.toValue()) .ifPresentOrElse( s -> load.accept(s, accumulator), () -> log.warn("Failed to coerce " + i + " into a filename when evaluating " + name)); } } } } } static void loadStructureFromDef(Log log, String name, Record structure, Value defDef, Function moldFromProperties) { final Properties properties = new Properties(); if (defDef instanceof Record) { for (Item i : defDef) { if (i instanceof Slot) { if (!(i.key() instanceof Text)) { log.warn("Ignored non-Text Slot key " + i.key() + " when loading " + name + " from def"); continue; } final String key = i.key().stringValue(); if (structure.get(key).isDistinct() || properties.getProperty(key) != null) { log.debug("Ignored already-defined key " + key + " when loading " + name + " from def"); continue; } if (isConfigFieldMold(i.toValue())) { evaluateConfigMold(log, key, i.toValue(), properties, structure); } else if (i.toValue().isDistinct()) { structure.updatedSlot(key, i.toValue()); } else { log.warn("Ignored non-string Slot value " + i.toValue() + " under " + key + " when loading " + name + " from def"); } } else { log.warn("Ignored non-Slot item " + i + " when loading " + name + " from def"); } } final Record mold = moldFromProperties.apply(properties); for (Item i : mold) { if (i instanceof Slot && !structure.get(i.key()).isDistinct()) { structure.updatedSlot(i.key(), i.toValue()); } } } else if (defDef.isDistinct()) { log.warn("Ignored non-Record def " + defDef + " when loading " + name); } } private static boolean isConfigFieldMold(Value mold) { return mold instanceof Record && mold.head() instanceof Attr && "config".equals(mold.head().key().stringValue(null)); } private static Optional evaluateConfigProp(Log log, String field, Value configMold) { final ConfigField cf = new ConfigField(log, field, configMold.get("env").stringValue(null), configMold.get("prop").stringValue(null), configMold.get("def").stringValue(null)); return cf.evaluate(); } private static void evaluateConfigMold(Log log, String field, Value configMold, Properties props, Record structure) { final ConfigField cf = new ConfigField(log, field, configMold.get("env").stringValue(null), configMold.get("prop").stringValue(null), configMold.get("def")); cf.evaluate(props, structure, configMold, field); } static class ConfigField { final Log log; final String field; final String env; final String prop; final String def; final Value defVal; private ConfigField(Log log, String field, String env, String prop, String def, Value defVal) { this.log = log; this.field = field; this.env = env; this.prop = prop; this.def = def; this.defVal = defVal; } ConfigField(Log log, String field, String env, String prop, String def) { this(log, field, env, prop, def, null); } ConfigField(Log log, String field, String env, String prop, Value def) { this(log, field, env, prop, null, def); } Optional evaluate() { return evaluateEnv() .or(this::evaluateProp) .or(this::evaluateDef); } void evaluate(Properties properties, Record structure, Value mold, String name) { evaluateEnv() .or(this::evaluateProp) .ifPresentOrElse( s -> properties.setProperty(this.field, s), () -> evaluateDefVal().ifPresentOrElse( v -> structure.slot(this.field, v), () -> this.log.warn("Ignored Field " + this.field + " that had no Value at runtime via " + mold + " when loading " + name))); } Optional evaluateEnv() { if (this.env == null || this.env.isEmpty()) { return Optional.empty(); } final String result = System.getenv(this.env); if (result == null) { this.log.debug("No env configured under " + this.env + ", falling back"); return Optional.empty(); } else { this.log.info("Used env " + this.env + " in @config interpretation"); return Optional.of(result); } } Optional evaluateProp() { if (this.prop == null || this.prop.isEmpty()) { return Optional.empty(); } final String result = System.getProperty(this.prop); if (result == null) { this.log.debug("No prop configured under " + this.prop + ", falling back"); return Optional.empty(); } else { this.log.info("Used prop " + this.prop + " in @config interpretation of " + this.field); return Optional.of(result); } } Optional evaluateDef() { return this.def == null ? Optional.empty() : Optional.of(this.def); } Optional evaluateDefVal() { return this.defVal == null ? Optional.empty() : Optional.of(this.defVal); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy