/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.struct; import groovy.lang.GroovyObject; import groovy.lang.MetaClass; import java.math.BigDecimal; import java.math.BigInteger; 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.regex.Pattern; import org.codehaus.groovy.runtime.InvokerHelper; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import divconq.lang.BigDateTime; import divconq.lang.Memory; import divconq.lang.op.FuncResult; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationResult; import divconq.schema.DataType; import divconq.schema.Field; import divconq.script.ExecuteState; import divconq.script.StackEntry; import divconq.struct.builder.BuilderStateException; import divconq.struct.builder.ICompositeBuilder; import divconq.struct.scalar.BooleanStruct; import divconq.struct.scalar.NullStruct; import divconq.struct.scalar.StringStruct; import divconq.util.ClassicIterableAdapter; import divconq.util.IAsyncIterable; import divconq.util.StringUtil; import divconq.xml.XElement; /** * DivConq uses a specialized type system that provides type consistency across services * (including web services), database fields and stored procedures, as well as scripting. * * All scalars (including primitives) and composites (collections) are wrapped by some * subclass of Struct. Map collections are expressed by this class - records have fields * and fields are a name value pair. This class is analogous to an Object in JSON but may * contain type information as well, similar to Yaml. * * TODO link to blog entries. * * @author Andy * */ public class RecordStruct extends CompositeStruct implements IItemCollection, GroovyObject /*, JSObject */ { // this defines valid field name pattern (same as json) // start with number should be ok static protected final Pattern FIELD_NAME_PATTERN = Pattern.compile("(^[a-zA-Z0-9\\$_\\-]*$)|(^[\\$_][a-zA-Z0-9\\$_\\-]*$)"); //Pattern.compile("(^[a-zA-Z][a-zA-Z0-9\\$_\\-]*$)|(^[\\$_][a-zA-Z][a-zA-Z0-9\\$_\\-]*$)"); // TODO check field names inside of "set field" etc. static public boolean validateFieldName(String v) { if (StringUtil.isEmpty(v)) return false; return !StringUtil.containsRestrictedChars(v); //RecordStruct.FIELD_NAME_PATTERN.matcher(v).matches(); } protected Map<String,FieldStruct> fields = new HashMap<String,FieldStruct>(); @Override public DataType getType() { if (this.explicitType != null) return super.getType(); // implied only, not explicit return OperationContext.get().getSchema().getType("AnyRecord"); } /** * Provide data type info (schema for fields) and a list of initial fields * * @param type field schema * @param fields initial pairs */ public RecordStruct(DataType type, FieldStruct... fields) { super(type); this.setField(fields); } /** * Optionally provide a list of initial fields * * @param fields initial pairs */ public RecordStruct(FieldStruct... fields) { // this is necessary so that Hub can start an initial context (setField causes a allocate guest) if (fields.length > 0) this.setField(fields); } /* (non-Javadoc) * @see divconq.struct.CompositeStruct#select(divconq.struct.PathPart[]) */ @Override public Struct select(PathPart... path) { if (path.length == 0) return this; PathPart part = path[0]; if (!part.isField()) { OperationResult log = part.getLog(); if (log != null) log.warnTr(504, this); return NullStruct.instance; } String fld = part.getField(); if (!this.fields.containsKey(fld)) { //OperationResult log = part.getLog(); //if (log != null) // log.warnTr(505, fld); return NullStruct.instance; } Struct o = this.getField(fld); if (path.length == 1) return (o != null) ? o : NullStruct.instance; if (o instanceof CompositeStruct) return ((CompositeStruct)o).select(Arrays.copyOfRange(path, 1, path.length)); OperationResult log = part.getLog(); if (log != null) log.warnTr(503, o); return NullStruct.instance; } /* (non-Javadoc) * @see divconq.struct.CompositeStruct#isEmpty() */ @Override public boolean isEmpty() { return (this.fields.size() == 0); } @Override public void toBuilder(ICompositeBuilder builder) throws BuilderStateException { builder.startRecord(); for (FieldStruct f : this.fields.values()) f.toBuilder(builder); builder.endRecord(); } /** * Adds or replaces a list of fields within the record. * * @param fields to add or replace * @return a log of messages about success of the call */ public void setField(FieldStruct... fields) { for (FieldStruct f : fields) { Struct svalue = f.getValue(); if (!f.prepped) { // take the original value and convert to a struct, fields hold structures Object value = f.orgvalue; if (value instanceof ICompositeBuilder) value = ((ICompositeBuilder)value).toLocal(); if (this.explicitType != null) { Field fld = this.explicitType.getField(f.getName()); if (fld != null) { Struct sv = fld.wrap(value); if (sv != null) svalue = sv; } } if (svalue == null) svalue = Struct.objectToStruct(value); f.setValue(svalue); f.prepped = true; } //FieldStruct old = this.fields.get(f.getName()); //if (old != null) // old.dispose(); this.fields.put(f.getName(), f); } } /** * Add or replace a specific field with a value. * * @param name of field * @param value to store with field * @return a log of messages about success of the call */ public void setField(String name, Object value) { this.setField(new FieldStruct(name, value)); } public RecordStruct withField(String name, Object value) { this.setField(new FieldStruct(name, value)); return this; } /** * * @return collection of all the fields this record holds */ public Iterable<FieldStruct> getFields() { return this.fields.values(); } /** * * @param name of the field desired * @return the struct for that field */ public Struct getField(String name) { if (!this.fields.containsKey(name)) return null; FieldStruct fs = this.fields.get(name); if (fs == null) return null; return fs.value; } /** * * @param name of the field desired * @return the struct for that field */ public FieldStruct getFieldStruct(String name) { if (!this.fields.containsKey(name)) return null; return this.fields.get(name); } /** * * @param from original name of the field * @param to new name for field */ public void renameField(String from, String to) { FieldStruct f = this.fields.remove(from); if (f != null) { f.setName(to); this.fields.put(to, f); } } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as an Object */ public Object getFieldAsAny(String name) { Struct st = this.getField(name); if (st == null) return null; if (st instanceof ScalarStruct) return ((ScalarStruct)st).getGenericValue(); if (st instanceof CompositeStruct) return ((CompositeStruct)st).toString(); return null; } /** * If the record has schema, lookup the schema for a given field. * * @param name of the field desired * @return field's schema */ public DataType getFieldType(String name) { // look first at the field value, if it has schema return Struct fs = this.getField(name); if ((fs != null) && (fs.hasExplicitType())) return fs.getType(); // look next at this records schema if (this.explicitType != null) { Field fld = this.explicitType.getField(name); if (fld != null) return fld.getPrimaryType(); } // give up, we don't know the schema return null; } /** * Like getField, except if the field does not exist it will be created and added * to the Record (unless that field name violates the schema). * * @param name of the field desired * @return log of messages from call plus the requested structure */ public FuncResult<Struct> getOrAllocateField(String name) { FuncResult<Struct> fr = new FuncResult<Struct>(); if (!this.fields.containsKey(name)) { Struct value = null; if (this.explicitType != null) { Field fld = this.explicitType.getField(name); if (fld != null) return fld.create(); if (this.explicitType.isAnyRecord()) value = NullStruct.instance; } else value = NullStruct.instance; if (value != null) { FieldStruct f = new FieldStruct(name, value); f.value = value; this.fields.put(name, f); fr.setResult(value); } } else { Struct value = this.getField(name); if (value == null) value = NullStruct.instance; fr.setResult(value); } return fr; } /** * * @param name of field * @return true if field exists */ public boolean hasField(String name) { return this.fields.containsKey(name); } /** * * @param name of field * @return true if field does not exist or if field is string and its value is empty */ public boolean isFieldEmpty(String name) { Struct f = this.getField(name); if (f == null) return true; return f.isEmpty(); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Integer (DivConq thinks of integers as 64bit) */ public Long getFieldAsInteger(String name) { return Struct.objectToInteger(this.getField(name)); } public long getFieldAsInteger(String name, long defaultval) { Long x = Struct.objectToInteger(this.getField(name)); if (x == null) return defaultval; return x; } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as BigInteger */ public BigInteger getFieldAsBigInteger(String name) { return Struct.objectToBigInteger(this.getField(name)); } public BigInteger getFieldAsBigInteger(String name, BigInteger defaultval) { BigInteger x = Struct.objectToBigInteger(this.getField(name)); if (x == null) return defaultval; return x; } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as BigDecimal */ public BigDecimal getFieldAsDecimal(String name) { return Struct.objectToDecimal(this.getField(name)); } public BigDecimal getFieldAsDecimal(String name, BigDecimal defaultval) { BigDecimal x = Struct.objectToDecimal(this.getField(name)); if (x == null) return defaultval; return x; } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Boolean */ public Boolean getFieldAsBoolean(String name) { return Struct.objectToBoolean(this.getField(name)); } public boolean getFieldAsBooleanOrFalse(String name) { Boolean b = Struct.objectToBoolean(this.getField(name)); return (b == null) ? false : b.booleanValue(); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as DateTime */ public DateTime getFieldAsDateTime(String name) { return Struct.objectToDateTime(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as BigDateTime */ public BigDateTime getFieldAsBigDateTime(String name) { return Struct.objectToBigDateTime(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Date */ public LocalDate getFieldAsDate(String name) { return Struct.objectToDate(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Time */ public LocalTime getFieldAsTime(String name) { return Struct.objectToTime(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as a String */ public String getFieldAsString(String name) { return Struct.objectToString(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Memory */ public Memory getFieldAsBinary(String name) { return Struct.objectToBinary(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as a Record */ public RecordStruct getFieldAsRecord(String name) { return Struct.objectToRecord(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as a List */ public ListStruct getFieldAsList(String name) { return Struct.objectToList(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as CompositeStruct */ public CompositeStruct getFieldAsComposite(String name) { return Struct.objectToComposite(this.getField(name)); } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Struct */ public Struct getFieldAsStruct(String name) { return Struct.objectToStruct(this.getField(name)); } public <T extends Object> T getFieldAsStruct(String name, Class<T> type) { Struct s = this.getField(name); if (type.isAssignableFrom(s.getClass())) return type.cast(s); return null; } /** * Unlike getField, this returns the value (inner) rather than struct wrapping * the value. * * @param name of the field desired * @return field's "inner" value as Xml (will parse if value is string) */ public XElement getFieldAsXml(String name) { return Struct.objectToXml(this.getField(name)); } /** * * @return number of fields held by this record */ public int getFieldCount() { return this.fields.size(); } /* public String checkRequiredFields(String... fields) { for (String fld : fields) { if (this.isFieldBlank(fld)) return fld; } return null; } public String checkRequiredIfPresentFields(String... fields) { for (String fld : fields) { if (this.hasField(fld) && this.isFieldBlank(fld)) return fld; } return null; } public String checkFieldRange(String... fields) { for (FieldStruct fld : this.getFields()) { boolean fnd = false; for (String fname : fields) { if (fld.getName().equals(fname)) { fnd = true; break; } } if (!fnd) return fld.getName(); } return null; } */ /** * * @param name of field to remove */ public FieldStruct removeField(String name) { return this.fields.remove(name); } public Struct sliceField(String name) { FieldStruct fld = this.fields.get(name); this.fields.remove(name); return fld.sliceValue(); } @Override protected void doCopy(Struct n) { super.doCopy(n); RecordStruct nn = (RecordStruct)n; for (FieldStruct fld : this.fields.values()) nn.setField(fld.deepCopy()); } @Override public Struct deepCopy() { RecordStruct cp = new RecordStruct(); this.doCopy(cp); return cp; } public RecordStruct deepCopyFields(String... include) { RecordStruct cp = new RecordStruct(); super.doCopy(cp); for (String fld : include) { if (this.hasField(fld)) cp.setField(this.fields.get(fld).deepCopy()); } return cp; } public RecordStruct deepCopyExclude(String... exclude) { RecordStruct cp = new RecordStruct(); super.doCopy(cp); for (FieldStruct fld : this.fields.values()) { boolean fnd = false; for (String x : exclude) if (fld.getName().equals(x)) { fnd = true; break; } if (!fnd) cp.setField(fld.deepCopy()); } return cp; } /** * Remove all child fields. */ @Override public void clear() { this.fields.clear(); } @Override public void operation(StackEntry stack, XElement code) { if ("Set".equals(code.getName())) { this.clear(); String json = stack.resolveValueToString(code.getText()); if (StringUtil.isNotEmpty(json)) { RecordStruct pjson = (RecordStruct) CompositeParser.parseJson(" { " + json + " } ").getResult(); this.copyFields(pjson); } stack.resume(); return; } if ("SetField".equals(code.getName())) { String def = stack.stringFromElement(code, "Type"); String name = stack.stringFromElement(code, "Name"); if (StringUtil.isEmpty(name)) { // TODO log stack.resume(); return; } Struct var = null; if (StringUtil.isNotEmpty(def)) var = stack.getActivity().createStruct(def); if (code.hasAttribute("Value")) { Struct var3 = stack.refFromElement(code, "Value"); if (var == null) var = stack.getActivity().createStruct(var3.getType().getId()); if (var instanceof ScalarStruct) ((ScalarStruct) var).adaptValue(var3); else var = var3; } if (var == null) { stack.setState(ExecuteState.Done); OperationContext.get().errorTr(520); stack.resume(); return; } this.setField(name, var); stack.resume(); return; } if ("RemoveField".equals(code.getName())) { String name = stack.stringFromElement(code, "Name"); if (StringUtil.isEmpty(name)) { // TODO log stack.resume(); return; } this.removeField(name); stack.resume(); return; } if ("NewList".equals(code.getName())) { String name = stack.stringFromElement(code, "Name"); if (StringUtil.isEmpty(name)) { // TODO log stack.resume(); return; } this.removeField(name); this.setField(name, new ListStruct()); stack.resume(); return; } if ("NewRecord".equals(code.getName())) { String name = stack.stringFromElement(code, "Name"); if (StringUtil.isEmpty(name)) { // TODO log stack.resume(); return; } this.removeField(name); this.setField(name, new RecordStruct()); stack.resume(); return; } if ("HasField".equals(code.getName())) { String name = stack.stringFromElement(code, "Name"); if (StringUtil.isEmpty(name)) { // TODO log stack.resume(); return; } String handle = stack.stringFromElement(code, "Handle"); if (handle != null) stack.addVariable(handle, new BooleanStruct(this.hasField(name))); stack.resume(); return; } if ("IsFieldEmpty".equals(code.getName())) { String name = stack.stringFromElement(code, "Name"); if (StringUtil.isEmpty(name)) { // TODO log stack.resume(); return; } String handle = stack.stringFromElement(code, "Handle"); if (handle != null) stack.addVariable(handle, new BooleanStruct(this.isFieldEmpty(name))); stack.resume(); return; } super.operation(stack, code); } public void copyFields(RecordStruct src, String... except) { if (src != null) for (FieldStruct fld : src.getFields()) { boolean fnd = false; for (String x : except) if (fld.getName().equals(x)) { fnd = true; break; } if (!fnd) this.setField(fld); } } @Override public Iterable<Struct> getItems() { List<String> tkeys = new ArrayList<String>(); for (String key : this.fields.keySet()) tkeys.add(key); Collections.sort(tkeys); List<Struct> keys = new ArrayList<Struct>(); for (String key : tkeys) keys.add(new StringStruct(key)); return keys; } @Override public IAsyncIterable<Struct> getItemsAsync() { return new ClassicIterableAdapter<Struct>(this.getItems()); } @Override public boolean equals(Object obj) { // TODO go deep if (obj instanceof RecordStruct) { RecordStruct data = (RecordStruct) obj; for (FieldStruct fld : this.fields.values()) { if (!data.hasField(fld.name)) return false; Struct ds = data.getField(fld.name); Struct ts = fld.value; if ((ds == null) && (ts == null)) continue; if ((ds == null) && (ts != null)) return false; if ((ds != null) && (ts == null)) return false; if (!ts.equals(ds)) return false; } // don't need to check match the other way around, we already know matching fields have good values for (FieldStruct fld : data.fields.values()) { if (!this.hasField(fld.name)) return false; } return true; } return super.equals(obj); } @Override public Object getProperty(String name) { Struct v = this.getField(name); if (v == null) return null; if (v instanceof CompositeStruct) return v; return ((ScalarStruct) v).getGenericValue(); } @Override public void setProperty(String name, Object value) { this.setField(name, value); } // TODO generate only on request private transient MetaClass metaClass = null; @Override public void setMetaClass(MetaClass v) { this.metaClass = v; } @Override public MetaClass getMetaClass() { if (this.metaClass == null) this.metaClass = InvokerHelper.getMetaClass(getClass()); return this.metaClass; } @Override public Object invokeMethod(String name, Object arg1) { // is really an object array Object[] args = (Object[])arg1; if (args.length > 0) System.out.println("G2: " + name + " - " + args[0]); else System.out.println("G2: " + name); // TODO Auto-generated method stub return null; } /* @Override public boolean hasMember(String name) { return this.hasField(name); } @Override public Object getMember(String name) { // TODO there is probably a better way... if ("getFieldAsInteger".equals(name)) { return new AbstractJSObject() { @Override public Object call(Object thiz, Object... args) { return RecordStruct.this.getFieldAsInteger((String)args[0]); // TODO saftey } }; } // TODO there is probably a better way... if ("cbTest".equals(name)) { return new AbstractJSObject() { @Override public Object call(Object thiz, Object... args) { System.out.println(args[0]); //ScriptFunction x = null; new Thread(() -> { try { ((ScriptFunction)args[0]).getBoundInvokeHandle(args[0]).invoke(new RecordStruct(new FieldStruct("Data", "atad"))); } catch (Throwable x) { System.out.println("Invoke Error: " + x); x.printStackTrace(); } }).start(); return null; } }; } Struct v = this.getField(name); if (v instanceof CompositeStruct) return v; return ((ScalarStruct) v).getGenericValue(); } @Override public void removeMember(String name) { this.removeField(name); } @Override public void setMember(String name, Object value) { this.setField(name, value); } @Override public Collection<Object> values() { System.out.println("call to values..."); return null; //this.fields.values(); TODO } @Override public Set<String> keySet() { System.out.println("call to keyset..."); return this.fields.keySet(); } @Override public Object call(Object thiz, Object... args) { System.out.println("call to call... " + thiz); return null; //super.call(thiz, args); TODO } @Override public Object eval(String s) { System.out.println("a"); // TODO Auto-generated method stub return null; } @Override public boolean isArray() { System.out.println("b"); // TODO Auto-generated method stub return false; } @Override public boolean isFunction() { System.out.println("c"); // TODO Auto-generated method stub return false; } @Override public Object newObject(Object... args) { System.out.println("d"); // TODO Auto-generated method stub return null; } @Override public String getClassName() { System.out.println("e"); // TODO Auto-generated method stub return null; } @Override public Object getSlot(int arg0) { System.out.println("f"); // TODO Auto-generated method stub return null; } @Override public boolean hasSlot(int arg0) { System.out.println("g"); // TODO Auto-generated method stub return false; } @Override public boolean isInstance(Object arg0) { System.out.println("h"); // TODO Auto-generated method stub return false; } @Override public boolean isInstanceOf(Object arg0) { System.out.println("i"); // TODO Auto-generated method stub return false; } @Override public boolean isStrictFunction() { System.out.println("j"); // TODO Auto-generated method stub return false; } @Override public void setSlot(int arg0, Object arg1) { System.out.println("k"); // TODO Auto-generated method stub } @Override public double toNumber() { System.out.println("l"); // TODO Auto-generated method stub return 0; } */ }