nstream.adapter.common.ext.JetTranslator Maven / Gradle / Ivy
Show all versions of nstream-adapter-common Show documentation
// 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.ext;
import java.util.Map;
import java.util.Properties;
import nstream.adapter.common.AdapterSettings;
import swim.recon.Recon;
import swim.structure.Attr;
import swim.structure.Form;
import swim.structure.Item;
import swim.structure.Record;
import swim.structure.Slot;
import swim.structure.Value;
import swim.util.Log;
public abstract class JetTranslator {
protected final String tag;
public JetTranslator(String tag) {
this.tag = tag;
}
/**
* Transforms an nstream-jet input, represented as the union of a baseline
* {@code T} and a {@code Properties}, into a finalized {@code T} and {@code
* Properties}.
*
* Because this function returns only a {@code T}, a change in the {@code
* Properties} is represented by modifying {@code unified} in-place.
*
* @param log the logger to use during translation
* @param settings the baseline {@code T} that represents a portion of the
* nstream-jet input
* @param unified the {@code Properties} that contain the union of all {@link
* nstream.adapter.common.provision.Provision Provision} configurations
* relevant to the adapter, plus any relevant jet-specific
* fields
*
* @return the finalized {@code T}, which may differ from {@code settings}
* depending on the contents of {@code unified}.
*/
public abstract T translate(Log log, T settings, Properties unified);
/**
* Creates the structural representation of a {@code T} equivalent to a
* provided {@code Properties}.
*
* @param log the logger to use during molding
* @param p the {@code Properties} representation of the desired structure,
* where keys may be in either
*
* @return the structural representation of the {@code T} as described by
* {@code p}
*/
public abstract Record moldFromProperties(Log log, Properties p);
protected final T translate(Log log, T settings, Form tForm, Properties unified) {
final Value mold = tForm.mold(settings).toValue();
final Record moldRecord = mold instanceof Record ? (Record) mold : Record.create();
return tForm.cast(strip(negotiateSchemas(log, settings, moldRecord, unified)));
}
/**
* Handles possibly conflicting information regarding content handling
* elements of nstream-jet input.
*
* For example, the {@code T} that nstream-jet yields may declare a {@link
* nstream.adapter.common.ingress.ValueAssembler ValueAssembler} (e.g. {@code
* nstream.adapter.avro.SwimAvroAssembler}) and a {@code contentTypeOverride}
* (e.g. JSON) that do not make sense together. Use this method to define
* "conflict" criteria and whether they should be elegantly handled or throw
* {@code Exceptions}.
*
* @param log the logger to use during negotiation
* @param settings the baseline {@code T} that represents a portion of the
* nstream-jet input
* @param mold a {@code Record} the structural representation of {@code
* settings}, which may be modified in place
* @param unified a singular {@code Properties} that represents the structure
* for relevant {@code Provisions}, which will be updated
* in-place if needed
*
* @return the content-pertaining conflict-free structure of the {@code T}
* to be used, which is an in-place-modified {@code mold}
* whenever possible and a newly-created {@code Record} otherwise
*/
protected abstract Record negotiateSchemas(Log log, T settings, Record mold, Properties unified);
protected Record setAvroGenericRecordAssembler(Log log, Record settingsMold,
String camelField, Value assembler,
String replacee) {
final Attr graAttr = Attr.of("valueAssembler", "nstream.adapter.avro.GenericRecordAssembler");
if (!(assembler instanceof Record)) {
log.info("Inferred " + camelField + " as GenericRecordAssembler");
settingsMold = settingsMold.updatedSlot(camelField, Record.create(1).item(graAttr));
} else if (!(assembler.head().equals(graAttr))) {
log.warn("Replaced " + camelField + " as GenericRecordAssembler from " + assembler.head()
+ " due to incompatibility with " + replacee);
settingsMold = settingsMold.updatedSlot(camelField, Record.create(1).item(graAttr));
}
return settingsMold;
}
protected Record setSwimAvroAssembler(Log log, Record settingsMold,
String camelField, Value assembler,
String avroSchema) {
if (avroSchema == null || avroSchema.isEmpty()) { // TODO: more validation possible here
log.warn("No avro schema found despite declared avro content type");
}
final Record saaRecord = Record.create(2)
.attr("valueAssembler", "nstream.adapter.avro.SwimAvroAssembler")
.slot("schema", avroSchema);
if (!(assembler instanceof Record)) {
log.info("Inferred " + camelField + " as " + saaRecord);
settingsMold = settingsMold.updatedSlot(camelField, saaRecord);
} else {
log.warn("Explicit valueAssembler " + assembler + " overrides schema " + saaRecord);
}
return settingsMold;
}
protected Record moldFromProperties(Log log, Properties p, Map map) {
Record result = Record.create(8);
result.attr(this.tag);
for (Map.Entry entry : map.entrySet()) {
final String k = entry.getKey();
result = entry.getValue().append(log, result, this.tag, k, p.getProperty(k, p.getProperty(camelToDot(k))));
}
return result;
}
protected static String camelToDot(String camel) {
if (camel == null || camel.isEmpty()) {
throw new IllegalArgumentException("Input string cannot be null or empty");
}
if (!isLowerCase(camel.charAt(0))) {
throw new IllegalArgumentException("Input string must start with a lowercase character");
}
final StringBuilder result = new StringBuilder();
for (int i = 0; i < camel.length(); i++) {
final char c = camel.charAt(i);
if (isUpperCase(c)) {
result.append('.').append(c + 'a' - 'A');
} else if (isLowerCase(c)) {
result.append(c);
} else {
throw new IllegalArgumentException("char " + c + " at idx " + i + " not camelCase-compatible");
}
}
return result.toString();
}
private static boolean isLowerCase(char c) {
return c >= 'a' && c <= 'z';
}
private static boolean isUpperCase(char c) {
return c >= 'A' && c <= 'Z';
}
protected static Record strip(Record input) {
final Record result = Record.create(input.length());
for (Item i : input) {
if (!(i instanceof Slot) || i.toValue().isDistinct()) {
result.add(i);
}
}
return result;
}
protected enum Entry {
LONG() {
@Override
String type() {
return "long";
}
@Override
Value mold(String prop) {
return Value.fromObject(Long.parseLong(prop));
}
},
LONGS() {
@Override
String type() {
return "CSV";
}
@Override
Value mold(String prop) {
final String[] split = prop.split(",");
if (split.length == 0) {
return Value.absent();
}
final Record result = Record.create(3);
for (String s : split) {
result.item(Long.parseLong(s));
}
return result;
}
},
STRING() {
String type() {
return "String";
}
@Override
Value mold(String prop) {
return Value.fromObject(prop);
}
},
STRINGS() {
String type() {
return "CSV(String)";
}
@Override
Value mold(String prop) {
final String[] split = prop.split(",");
if (split.length == 0) {
return Value.absent();
}
final Record result = Record.create(3);
for (String s : split) {
result.item(s);
}
return result;
}
},
RECON() {
@Override
String type() {
return "recon";
}
@Override
Value mold(String prop) {
return Recon.parse(prop);
}
};
abstract String type();
abstract Value mold(String prop);
Record append(Log log, Record soFar, String t, String field, String prop) {
if (prop != null) {
try {
final Value result = mold(prop);
if (result.isDefined()) {
soFar.slot(field, result);
}
} catch (Exception e) {
log.warn("Ignored " + t + "." + field + " configuration " + prop + " as it does not yield " + type());
}
}
return soFar;
}
}
}