/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.eas.client.model.application;
import com.eas.client.changes.Change;
import com.eas.client.events.PublishedSourcedEvent;
import com.eas.client.metadata.Field;
import com.eas.client.metadata.Fields;
import com.eas.client.metadata.Parameter;
import com.eas.client.metadata.Parameters;
import com.eas.client.model.Entity;
import com.eas.client.model.Relation;
import com.eas.client.queries.Query;
import com.eas.script.AlreadyPublishedException;
import com.eas.script.EventMethod;
import com.eas.script.HasPublished;
import com.eas.script.NoPublisherException;
import com.eas.script.ScriptFunction;
import com.eas.script.Scripts;
import com.eas.util.ListenerRegistration;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdk.nashorn.api.scripting.AbstractJSObject;
import jdk.nashorn.api.scripting.JSObject;
/**
*
* @author mg
* @param <M>
* @param <Q>
* @param <E>
*/
public abstract class ApplicationEntity<M extends ApplicationModel<E, Q>, Q extends Query, E extends ApplicationEntity<M, Q, E>> extends Entity<M, Q, E> implements HasPublished {
public static final String BAD_FIELD_NAME_MSG = "Bad field name %s";
public static final String BAD_FIND_AGRUMENTS_MSG = "Bad find agruments";
public static final String BAD_FIND_ARGUMENT_MSG = "Argument at index %d must be a rowset's field.";
public static final String BAD_PRIMARY_KEYS_MSG = "Bad primary keys detected. Required one and only one primary key field, but %d found.";
public static final String CANT_CONVERT_TO_MSG = "Can't convert to %s, substituting with null.";
// for runtime
protected JSObject onRequeried;
//
protected JSObject lastSnapshot = Scripts.getSpace() != null ? Scripts.getSpace().makeArray() : null;
protected JSObject snapshotConsumer;
protected JSObject snapshotProducer;
//
protected JSObject published;
protected ListenerRegistration cursorListener;
protected boolean valid;
protected Future<Void> pending;
public ApplicationEntity() {
super();
}
public ApplicationEntity(M aModel) {
super(aModel);
}
public ApplicationEntity(String aEntityId) {
super(aEntityId);
}
public boolean isValid() {
return valid;
}
public void invalidate() {
valid = false;
}
protected class RowsetRefreshTask implements Future<Void> {
protected boolean cancelled;
protected Consumer<Exception> onCancel;
public RowsetRefreshTask(Consumer<Exception> aOnCancel) {
super();
onCancel = aOnCancel;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
cancelled = true;
valid = true;
pending = null;
String msg = "Canceled query on " + (name != null && !name.isEmpty() ? name : "") + (title != null && !title.isEmpty() ? "[" + title + "]" : "");
Exception ex = new InterruptedException(msg);
model.terminateProcess((E) ApplicationEntity.this, ex);
if (onCancel != null) {
onCancel.accept(ex);
}
return cancelled;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public boolean isDone() {
return false;
}
@Override
public Void get() throws InterruptedException, ExecutionException {
return null;
}
@Override
public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return null;
}
}
public boolean isPending() {
return pending != null;
}
public void unpend() {
if (pending != null) {
pending.cancel(true);
pending = null;
}
}
protected void silentUnpend() {
ApplicationModel.RequeryProcess<E, Q> p = model.process;
model.process = null;
try {
unpend();
} finally {
model.process = p;
}
}
public void execute() throws Exception {
execute(null, null);
}
public void execute(JSObject aOnSuccess) throws Exception {
execute(aOnSuccess, null);
}
private static final String EXECUTE_JSDOC = ""
+ "/**\n"
+ " * Requeries the entity, only if any of its parameters has changed.\n"
+ " * @param onSuccess The handler function for refresh data on success event (optional).\n"
+ " * @param onFailure The handler function for refresh data on failure event (optional).\n"
+ " */";
@ScriptFunction(jsDoc = EXECUTE_JSDOC, params = {"onSuccess", "onFailure"})
public void execute(final JSObject aOnSuccess, final JSObject aOnFailure) throws Exception {
if (aOnSuccess != null) {
if (getOutRelations().isEmpty()) {
internalExecute((JSObject v) -> {
aOnSuccess.call(null, new Object[]{published});
}, aOnFailure != null ? (Exception ex) -> {
aOnFailure.call(null, new Object[]{ex.getMessage()});
} : null);
} else {
model.inProcess(() -> {
internalExecute(null, null);
return null;
}, (Void v) -> {
aOnSuccess.call(null, new Object[]{published});
}, aOnFailure != null ? (Exception ex) -> {
aOnFailure.call(null, new Object[]{ex.getMessage()});
} : null);
}
} else {
internalExecute(null, null);
}
}
// Requery interface
public void requery() throws Exception {
requery(null, null);
}
public void requery(JSObject aOnSuccess) throws Exception {
requery(aOnSuccess, null);
}
private static final String REQUERY_JSDOC = ""
+ "/**\n"
+ " * Requeries the entity's data. Forses the entity to refresh its data, no matter if its parameters has changed or not.\n"
+ " * @param onSuccess The callback function for refreshed data on success event (optional).\n"
+ " * @param onFailure The callback function for refreshed data on failure event (optional).\n"
+ " */";
@ScriptFunction(jsDoc = REQUERY_JSDOC, params = {"onSuccess", "onFailure"})
public void requery(JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
invalidate();
execute(aOnSuccess, aOnFailure);
}
private static final String QUERY_JSDOC = ""
+ "/**\n"
+ " * Queries the entity's data. Data will be fresh copy. A call to query() will be independent from other calls.\n"
+ " * Subsequent calls will not cancel requests made within previous calls.\n"
+ " * @param params The params object with parameters' values of query. These values will not be written to entity's parameters.\n"
+ " * @param onSuccess The callback function for fresh data on success event (optional).\n"
+ " * @param onFailure The callback function for fresh data on failure event (optional).\n"
+ " */";
@ScriptFunction(jsDoc = QUERY_JSDOC, params = {"params", "onSuccess", "onFailure"})
public JSObject query(JSObject aParams, JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
Query copied = query.copy();
aParams.keySet().forEach((String pName) -> {
Parameter p = copied.getParameters().get(pName);
if (p != null) {
Object jsValue = aParams.getMember(pName);
p.setValue(Scripts.getSpace().toJava(jsValue));
}
});
return copied.execute(Scripts.getSpace(), aOnSuccess != null ? (JSObject v) -> {
aOnSuccess.call(null, new Object[]{v});
} : null, aOnFailure != null ? (Exception ex) -> {
aOnFailure.call(null, new Object[]{ex.getMessage()});
} : null);
}
private static final String APPEND_JSDOC = ""
+ "/**\n"
+ " * Append data to the entity's data.\n"
+ " * Appended data will be managed by model, but appending itself will not be included in data changelog.\n"
+ " * @param data The plain js objects array to be appended.\n"
+ " */";
@ScriptFunction(jsDoc = APPEND_JSDOC, params = {"data"})
public void append(JSObject aData) {
if (snapshotConsumer != null) {
snapshotConsumer.call(null, new Object[]{aData, false});
}
}
private static final String INSTANCE_CONSTRUCTOR_JSDOC = ""
+ "/**\n"
+ " * The constructor funciton for the entity's data array elements.\n"
+ " */";
@ScriptFunction(jsDoc = INSTANCE_CONSTRUCTOR_JSDOC)
public JSObject getElementClass() {
return getFields().getInstanceConstructor();
}
@ScriptFunction
public void setElementClass(JSObject aValue) {
getFields().setInstanceConstructor(aValue);
}
public void putOrmScalarDefinition(String aName, Fields.OrmDef aDefinition) {
if (aName != null && !aName.isEmpty() && aDefinition != null) {
Map<String, Fields.OrmDef> defs = getFields().getOrmScalarDefinitions();
if (!defs.containsKey(aName)) {
getFields().putOrmScalarDefinition(aName, aDefinition);
} else {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.FINE, String.format("ORM property %s redefinition attempt on entity %s %s.", aName, name != null && !name.isEmpty() ? name : "", title != null && !title.isEmpty() ? "[" + title + "]" : ""));
}
}
}
public Map<String, Fields.OrmDef> getOrmScalarDefinitions() {
return getFields().getOrmScalarDefinitions();
}
public void putOrmCollectionDefinition(String aName, Fields.OrmDef aDefinition) {
if (aName != null && !aName.isEmpty() && aDefinition != null) {
Map<String, Fields.OrmDef> defs = getFields().getOrmCollectionsDefinitions();
if (!defs.containsKey(aName)) {
getFields().putOrmCollectionDefinition(aName, aDefinition);
} else {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.FINE, String.format("ORM property %s redefinition attempt on entity %s %s.", aName, name != null && !name.isEmpty() ? name : "", title != null && !title.isEmpty() ? "[" + title + "]" : ""));
}
}
}
public Map<String, Fields.OrmDef> getOrmCollectionsDefinitions() {
return getFields().getOrmCollectionsDefinitions();
}
@Override
public JSObject getPublished() {
if (published == null) {
JSObject publisher = Scripts.getSpace().getPublisher(this.getClass().getName());
if (publisher == null || !publisher.isFunction()) {
throw new NoPublisherException();
}
published = (JSObject) publisher.call(null, new Object[]{this});
}
return published;
}
@Override
public void setPublished(JSObject aValue) {
if (published != null && Scripts.isInitialized()) {
throw new AlreadyPublishedException();
}
published = aValue;
if (Scripts.isInitialized()) {
Scripts.getSpace().listen(published, "cursor", new AbstractJSObject() {
@Override
public boolean isFunction() {
return true;
}
@Override
public Object call(Object thiz, Object... args) {
try {
resignOnCursor();
internalExecuteChildren(false);
} catch (Exception ex) {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
});
}
}
/**
* Returns change log for this entity. In some cases, we might have several
* change logs in one model. Several databases is the case.
*
* @throws java.lang.Exception
* @return
*/
public abstract List<Change> getChangeLog() throws Exception;
private static final String ON_REQUIRED_JSDOC = ""
+ "/**\n"
+ " * The handler function for the event occured after the entity's data have been requeried.\n"
+ " */";
@ScriptFunction(jsDoc = ON_REQUIRED_JSDOC)
@EventMethod(eventClass = PublishedSourcedEvent.class)
public JSObject getOnRequeried() {
return onRequeried;
}
@ScriptFunction
public void setOnRequeried(JSObject aValue) {
JSObject oldValue = onRequeried;
onRequeried = aValue;
changeSupport.firePropertyChange("onRequeried", oldValue, aValue);
}
public JSObject getSnapshotConsumer() {
return snapshotConsumer;
}
public void setSnapshotConsumer(JSObject aValue) {
snapshotConsumer = aValue;
}
public JSObject getSnapshotProducer() {
return snapshotProducer;
}
public void setSnapshotProducer(JSObject aValue) {
snapshotProducer = aValue;
}
protected static final String ENQUEUE_UPDATE_JSDOC = ""
+ "/**\n"
+ " * Adds the updates into the change log as a command.\n"
+ " * @param params Params object literal. Optional. If absent, entity's parameters' values will be taken.\n"
+ " */";
public abstract void enqueueUpdate(JSObject params) throws Exception;
protected static final String UPDATE_JSDOC = ""
+ "/**\n"
+ " * Applies the updates into the database and commits the transaction.\n"
+ " * @param params Params object literal.\n"
+ " * @param onSuccess Success callback. It has an argument, - updates rows count.\n"
+ " * @param onFailure Failure callback. It has an argument, - exception occured while applying updates into the database.\n"
+ " */";
public abstract int update(JSObject params, JSObject onSuccess, JSObject onFailure) throws Exception;
protected static final String EXECUTE_UPDATE_JSDOC = ""
+ "/**\n"
+ " * Applies the updates into the database and commits the transaction.\n"
+ " * @param onSuccess Success callback. It has an argument, - updates rows count.\n"
+ " * @param onFailure Failure callback. It has an argument, - exception occured while applying updates into the database.\n"
+ " */";
public abstract int executeUpdate(JSObject onSuccess, JSObject onFailure) throws Exception;
protected void internalExecute(final Consumer<JSObject> aOnSuccess, final Consumer<Exception> aOnFailure) throws Exception {
if (query == null) {
throw new IllegalStateException("Query must present. Query name: " + queryName + "; tableName: " + getTableNameForDescription());
}
bindQueryParameters();
if (isValid()) {
if (aOnSuccess != null) {
Scripts.getSpace().process(() -> {
aOnSuccess.accept(null);
});
}
} else {
// Requery if query parameters values have been changed while bindQueryParameters() call
// or we are forced to refresh the data via requery() call.
silentUnpend();
JSObject jsRowset = refreshRowset(aOnSuccess, aOnFailure);
assert pending != null || (aOnSuccess == null && model.process == null);
}
}
protected void internalExecuteChildren(boolean refresh) throws Exception {
Set<Relation<E>> rels = getOutRelations();
if (rels != null) {
Set<E> toExecute = new HashSet<>();
rels.forEach((Relation<E> outRel) -> {
if (outRel != null) {
E rEntity = outRel.getRightEntity();
if (rEntity != null) {
toExecute.add(rEntity);
}
}
});
model.executeEntities(refresh, toExecute);
}
}
protected void internalExecuteChildren(boolean refresh, int aOnlyFieldIndex) throws Exception {
Set<Relation<E>> rels = getOutRelations();
if (rels != null) {
Field onlyField = getFields().get(aOnlyFieldIndex);
Set<E> toExecute = new HashSet<>();
rels.forEach((Relation<E> outRel) -> {
if (outRel != null) {
E rEntity = outRel.getRightEntity();
if (rEntity != null && outRel.getLeftField() == onlyField) {
toExecute.add(rEntity);
}
}
});
model.executeEntities(refresh, toExecute);
}
}
protected static final String PENDING_ASSUMPTION_FAILED_MSG = "pending assigned to null without pending.cancel() call.";
protected JSObject refreshRowset(final Consumer<JSObject> aOnSuccess, final Consumer<Exception> aOnFailure) throws Exception {
if (model.process != null || aOnSuccess != null) {
Future<Void> f = new RowsetRefreshTask(aOnFailure);
query.execute(Scripts.getSpace(), (JSObject aRowset) -> {
if (!f.isCancelled()) {
applySnapshot(aRowset);
assert pending == f : PENDING_ASSUMPTION_FAILED_MSG;
valid = true;
pending = null;
model.terminateProcess((E) ApplicationEntity.this, null);
fireRequeried();
if (aOnSuccess != null) {
aOnSuccess.accept(aRowset);
}
}
}, (Exception ex) -> {
Logger.getLogger(ApplicationPlatypusEntity.class.getName()).log(Level.SEVERE, ex.getMessage());
if (!f.isCancelled()) {
assert pending == f : PENDING_ASSUMPTION_FAILED_MSG;
valid = true;
pending = null;
model.terminateProcess((E) ApplicationEntity.this, ex);
if (aOnFailure != null) {
aOnFailure.accept(ex);
}
}
});
pending = f;
return null;
} else {
JSObject jsRowset = query.execute(Scripts.getSpace(), null, null);
applySnapshot(jsRowset);
fireRequeried();
return jsRowset;
}
}
public void takeSnapshot() {
if (snapshotProducer != null) {
lastSnapshot = (JSObject) snapshotProducer.call(null, new Object[]{});
}
}
public void applyLastSnapshot() {
applySnapshot(lastSnapshot);
}
public void applySnapshot(JSObject aValue) {
lastSnapshot = aValue;
// Apply aRowset as a snapshot. Be aware of change log!
if (snapshotConsumer != null) {// snapshotConsumer is null in designer
snapshotConsumer.call(null, new Object[]{aValue, true});
}
}
protected void fireRequeried() {
if (onRequeried != null) {
try {
JSObject event = Scripts.getSpace().makeObj();
event.setMember("source", published);
onRequeried.call(published, new Object[]{event});
} catch (Exception ex) {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
protected boolean isQueriable() throws Exception {
return queryName != null || (tableName != null && !tableName.isEmpty());
}
public void bindQueryParameters() throws Exception {
Parameters selfParameters = getQuery().getParameters();
for (int i = 1; i <= selfParameters.getFieldsCount(); i++) {
Parameter p = selfParameters.get(i);
boolean oldModified = p.isModified();
// Let's correct script evil!!!
p.setValue(Scripts.getSpace().toJava(p.getValue()));
p.setModified(oldModified);
}
//
boolean parametersModified = false;
Set<Relation<E>> inRels = getInRelations();
if (inRels != null && !inRels.isEmpty()) {
for (Relation<E> relation : inRels) {
if (relation != null && relation.isRightParameter()) {
E leftEntity = relation.getLeftEntity();
if (leftEntity != null) {
Object pValue = null;
if (relation.isLeftField()) {
// There might be entities - parameters values sources, with no
// data in theirs rowsets, so we can't bind query parameters to proper values. In the
// such case we initialize parameters values with null
JSObject leftRowset = leftEntity.getPublished();
if (leftRowset != null && leftRowset.getMember(CURSOR_PROP_NAME) instanceof JSObject) {
JSObject jsCursor = (JSObject) leftRowset.getMember(CURSOR_PROP_NAME);
pValue = Scripts.getSpace().toJava(jsCursor.getMember(relation.getLeftField().getName()));
} else {
pValue = null;
}
} else {
/*
Query<?> leftQuery = leftEntity.getQuery();
assert leftQuery != null : "Left query must present (Relation points to query, but query is absent)";
Parameters leftParams = leftQuery.getParameters();
assert leftParams != null : "Parameters of left query must present (Relation points to query parameter, but query parameters are absent)";
*/
Parameter leftParameter = relation.getLeftParameter();
if (leftParameter != null) {
pValue = leftParameter.getValue();
// Let's correct nashorn evil!!!
pValue = Scripts.getSpace().toJava(pValue);
if (pValue == null) {
pValue = leftParameter.getDefaultValue();
}
pValue = Scripts.getSpace().toJava(pValue);
} else {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.SEVERE, "Parameter of left query must present (Relation points to query parameter in entity: {0} [{1}], but query parameter is absent)", new Object[]{getTitle(), String.valueOf(getEntityId())});
}
}
Parameter selfPm = relation.getRightParameter();
if (selfPm != null) {
Object selfValue = selfPm.getValue();
if (!Objects.equals(selfValue, pValue)) {
selfPm.setValue(pValue);
}
}
} else {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.SEVERE, "Relation with no left entity detected");
}
}
}
}
for (int i = 1; i <= selfParameters.getFieldsCount(); i++) {
Parameter param = (Parameter) selfParameters.get(i);
if (param.isModified()) {
parametersModified = true;
param.setModified(false);
}
}
if (parametersModified) {
invalidate();
}
}
public Object executeScriptEvent(final JSObject aHandler, final PublishedSourcedEvent aEvent) {
Object res = null;
if (aHandler != null) {
try {
return Scripts.getSpace().toJava(aHandler.call(getPublished(), new Object[]{aEvent.getPublished()}));
} catch (Exception ex) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, ex.getMessage(), ex);
}
}
return res;
}
protected void resignOnCursor() {
if (cursorListener != null) {
cursorListener.remove();
cursorListener = null;
}
if (published != null && published.getMember(CURSOR_PROP_NAME) instanceof JSObject) {
JSObject jsCursor = (JSObject) published.getMember(CURSOR_PROP_NAME);
JSObject jsReg = Scripts.getSpace().listen(jsCursor, "", new AbstractJSObject() {
@Override
public boolean isFunction() {
return true;
}
@Override
public Object call(Object thiz, Object... args) {
try {
internalExecuteChildren(false);
} catch (Exception ex) {
Logger.getLogger(ApplicationEntity.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
});
cursorListener = () -> {
Scripts.unlisten(jsReg);
};
}
}
protected static final String CURSOR_PROP_NAME = "cursor";
@Override
protected void assign(E appTarget) throws Exception {
super.assign(appTarget);
appTarget.setOnRequeried(onRequeried);
}
@Override
public boolean addInRelation(Relation<E> aRelation) {
if (aRelation instanceof ReferenceRelation<?>) {
return false;
} else {
return super.addInRelation(aRelation);
}
}
@Override
public boolean addOutRelation(Relation<E> aRelation) {
if (aRelation instanceof ReferenceRelation<?>) {
return false;
} else {
return super.addOutRelation(aRelation);
}
}
}