/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.eas.client.model.application;
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.Model;
import com.eas.client.model.Relation;
import com.eas.client.queries.QueriesProxy;
import com.eas.client.queries.Query;
import com.eas.script.AlreadyPublishedException;
import com.eas.script.HasPublished;
import com.eas.script.NoPublisherException;
import com.eas.script.ScriptFunction;
import com.eas.script.Scripts;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdk.nashorn.api.scripting.JSObject;
/**
*
* @author mg
* @param <E>
* @param <Q>
*/
public abstract class ApplicationModel<E extends ApplicationEntity<?, Q, E>, Q extends Query> extends Model<E, Q> implements HasPublished {
protected JSObject published;
protected Set<ReferenceRelation<E>> referenceRelations = new HashSet<>();
protected QueriesProxy<Q> queries;
protected RequeryProcess<E, Q> process;
public static class RequeryProcess<E extends ApplicationEntity<?, Q, E>, Q extends Query> {
public Map<E, Exception> errors = new HashMap<>();
public Consumer<Void> onSuccess;
public Consumer<Exception> onFailure;
public RequeryProcess(Consumer<Void> aOnSuccess, Consumer<Exception> aOnFailure) {
super();
onSuccess = aOnSuccess;
assert onSuccess != null : "aOnSuccess argument is required.";
onFailure = aOnFailure;
}
protected String assembleErrors() {
if (errors != null && !errors.isEmpty()) {
StringBuilder sb = new StringBuilder();
errors.entrySet().stream().forEach((Map.Entry<E, Exception> entry) -> {
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(entry.getValue().getMessage()).append(" (").append(entry.getKey().getName()).append("[ ").append(entry.getKey().getTitle()).append("])");
});
return sb.toString();
}
return null;
}
public void cancel() throws Exception {
if (onFailure != null) {
onFailure.accept(new InterruptedException("Canceled"));
}
}
public void success() {
onSuccess.accept(null);
}
public void failure() {
if (onFailure != null) {
onFailure.accept(new Exception(assembleErrors()));
}
}
public void end() {
if (errors.isEmpty()) {
success();
} else {
failure();
}
}
}
public ApplicationModel() {
super();
}
public ApplicationModel(QueriesProxy<Q> aQueries) {
super();
queries = aQueries;
}
public QueriesProxy<Q> getQueries() {
return queries;
}
public void terminateProcess(E aSource, Exception aErrorMessage) {
if (process != null) {
if (aErrorMessage != null) {
process.errors.put(aSource, aErrorMessage);
}
if (!isPending()) {
RequeryProcess<E, Q> pr = process;
process = null;
pr.end();
}
}
}
public boolean isPending() {
return entities.values().stream().anyMatch((entity) -> (entity.isPending()));
}
public void executeEntities(boolean refresh, Set<E> toExecute) throws Exception {
if (refresh) {
toExecute.stream().forEach((entity) -> {
entity.invalidate();
});
}
for (E entity : toExecute) {
entity.internalExecute(null, null);
}
}
private Set<E> rootEntities() {
final Set<E> rootEntities = new HashSet<>();
entities.values().stream().forEach((E entity) -> {
if (entity.getInRelations().isEmpty()) {
rootEntities.add(entity);
}
});
return rootEntities;
}
/**
* Validates queries in force way. Such case is used in designer ONLY!
*
* @return
* @throws Exception
*/
@Override
protected boolean validateEntities() throws Exception {
for (E e : entities.values()) {
queries.getQuery(e.getQueryName(), Scripts.getSpace(), null, null);
}
return super.validateEntities();
}
public void validateQueries() throws Exception {
for (E entity : entities.values()) {
entity.validateQuery();
}
}
public Collection<E> entities() {
return entities.values();
}
@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) {
throw new AlreadyPublishedException();
}
published = aValue;
}
public void createORMDefinitions() {
referenceRelations.stream().forEach((ReferenceRelation<E> aRelation) -> {
String scalarPropertyName = aRelation.getScalarPropertyName();
String collectionPropertyName = aRelation.getCollectionPropertyName();
if (scalarPropertyName != null && !scalarPropertyName.isEmpty()) {
aRelation.getLeftEntity().putOrmScalarDefinition(scalarPropertyName,
new Fields.OrmDef(aRelation.getLeftField().getName(), scalarPropertyName, collectionPropertyName, Scripts.getSpace().scalarPropertyDefinition(
(JSObject) aRelation.getRightEntity().getPublished(),
aRelation.getRightField().getName(),
aRelation.getLeftField().getName())));
}
if (collectionPropertyName != null && !collectionPropertyName.isEmpty()) {
aRelation.getRightEntity().putOrmCollectionDefinition(collectionPropertyName,
new Fields.OrmDef(collectionPropertyName, scalarPropertyName, Scripts.getSpace().collectionPropertyDefinition(
(JSObject) aRelation.getLeftEntity().getPublished(),
aRelation.getRightField().getName(),
aRelation.getLeftField().getName())));
}
});
}
@Override
public Model<E, Q> copy() throws Exception {
Model<E, Q> copied = super.copy();
for (ReferenceRelation<E> relation : referenceRelations) {
ReferenceRelation<E> rcopied = (ReferenceRelation<E>) relation.copy();
copied.resolveRelation(rcopied);
((ApplicationModel<E, Q>) copied).getReferenceRelations().add(rcopied);
}
((ApplicationModel<E, Q>) copied).checkReferenceRelationsIntegrity();
return copied;
}
@Override
protected void validateRelations() throws Exception {
super.validateRelations();
for (Relation<E> rel : referenceRelations) {
resolveRelation(rel);
}
}
@Override
public void checkRelationsIntegrity() {
super.checkRelationsIntegrity();
checkReferenceRelationsIntegrity();
}
protected void checkReferenceRelationsIntegrity() {
List<ReferenceRelation<E>> toDel = new ArrayList<>();
referenceRelations.stream().forEach((ReferenceRelation<E> rel) -> {
if (rel.getLeftEntity() == null || (rel.getLeftField() == null && rel.getLeftParameter() == null)
|| rel.getRightEntity() == null || (rel.getRightField() == null && rel.getRightParameter() == null)) {
toDel.add(rel);
}
});
toDel.stream().forEach((rel) -> {
removeReferenceRelation(rel);
});
}
public abstract boolean isModified() throws Exception;
protected static final String SAVE_JSDOC = ""
+ "/**\n"
+ " * Saves model data changes.\n"
+ " * If model can't apply the changed data, than exception is thrown. In this case, application can call model.save() another time to save the changes.\n"
+ " * If an application needs to abort further attempts and discard model data changes, use <code>model.revert()</code>.\n"
+ " * Note, that a <code>model.save()</code> call on unchanged model nevertheless leads to a commit.\n"
+ " * @param onSuccess The function to be invoked after the data changes saved (optional).\n"
+ " * @param onFailure The function to be invoked when exception raised while commit process (optional).\n"
+ " */";
@ScriptFunction(jsDoc = SAVE_JSDOC, params = {"onSuccess", "onFailure"})
public int save(JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
if (aOnSuccess != null) {
commit((Integer aResult) -> {
commited();
aOnSuccess.call(null, new Object[]{aResult});
}, (Exception ex) -> {
rolledback(ex);
if (aOnFailure != null) {
aOnFailure.call(null, new Object[]{ex.getMessage()});
}
});
return 0;
} else {
try {
int result = commit(null, null);
commited();
return result;
} catch (Exception ex) {
rolledback(ex);
throw ex;
}
}
}
protected void rolledback(Exception ex) {
Logger.getLogger(ApplicationModel.class.getName()).log(Level.SEVERE, ex.toString());
}
public void save(JSObject aOnSuccess) throws Exception {
save(aOnSuccess, null);
}
public void save() throws Exception {
save(null, null);
}
protected static final String REVERT_JSDOC = ""
+ "/**\n"
+ " * Reverts model data changes.\n"
+ " * After this method call, no data changes are avaliable for <code>model.save()</code> method.\n"
+ " */";
@ScriptFunction(jsDoc = REVERT_JSDOC)
public void revert() {
entities.values().stream().forEach((E aEntity) -> {
aEntity.applyLastSnapshot();
});
}
public abstract int commit(Consumer<Integer> onSuccess, Consumer<Exception> onFailure) throws Exception;
public void commited() {
entities.values().stream().forEach((E aEntity) -> {
aEntity.takeSnapshot();
});
}
public final void requery() throws Exception {
requery(null, null);
}
public void requery(JSObject onSuccess) throws Exception {
requery(onSuccess, null);
}
protected static final String REQUERY_JSDOC = ""
+ "/**\n"
+ " * Requeries the model data. Forces the model data refresh, no matter if its parameters has changed or not.\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 = REQUERY_JSDOC, params = {"onSuccess", "onFailure"})
public void requery(JSObject onSuccess, JSObject onFailure) throws Exception {
inProcess(() -> {
revert();
executeEntities(true, rootEntities());
return null;
}, onSuccess != null ? (Void v) -> {
onSuccess.call(null, new Object[]{});
} : null, onFailure != null ? (Exception ex) -> {
onFailure.call(null, new Object[]{ex.getMessage()});
} : null);
}
public void execute() throws Exception {
execute(null, null);
}
public void execute(Consumer<Void> onSuccess) throws Exception {
execute(onSuccess, null);
}
private static final String EXECUTE_JSDOC = ""
+ "/**\n"
+ " * Refreshes the model, 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(Consumer<Void> onSuccess, Consumer<Exception> onFailure) throws Exception {
inProcess(() -> {
executeEntities(false, rootEntities());
return null;
}, onSuccess, onFailure);
}
public void inProcess(Callable<Void> aAction, Consumer<Void> onSuccess, Consumer<Exception> onFailure) throws Exception {
if (process != null) {
process.cancel();
process = null;
}
if (onSuccess != null) {
process = new RequeryProcess<>(onSuccess, onFailure);
}
aAction.call();
if (!isPending() && process != null) {
process.end();
process = null;
}
}
protected static final String USER_DATASOURCE_NAME = "userQuery";
public E createQuery(String aQueryName) throws Exception {
Logger.getLogger(ApplicationModel.class.getName()).log(Level.WARNING, "createQuery deprecated call detected. Use loadEntity() instead.");
return loadEntity(aQueryName);
}
private static final String LOAD_ENTITY_JSDOC = ""
+ "/**\n"
+ " * Creates new entity of model, based on application query.\n"
+ " * @param queryName the query application element name.\n"
+ " * @return a new entity.\n"
+ " */";
@ScriptFunction(jsDoc = LOAD_ENTITY_JSDOC, params = {"queryName"})
public E loadEntity(String aQueryName) throws Exception {
E entity = newGenericEntity();
entity.setName(USER_DATASOURCE_NAME);
entity.setQueryName(aQueryName);
entity.validateQuery();
return entity;
}
public E createQuery(E aLeftEntity, Field aLeftField, String aRightQueryId, Field aRightField) throws Exception {
E rightEntity = newGenericEntity();
rightEntity.setName(USER_DATASOURCE_NAME);
rightEntity.setQueryName(aRightQueryId);
addEntity(rightEntity);
// filter relation
Relation<E> rel = new Relation<>(aLeftEntity, aLeftField, rightEntity, aRightField);
addRelation(rel);
// parameters bypass relations
Parameters params = aLeftEntity.getQuery().getParameters();
assert params != null;
for (int i = 1; i <= params.getParametersCount(); i++) {
Parameter p = (Parameter) params.get(i);
Relation<E> pRel = new Relation<>(aLeftEntity, p, rightEntity, rightEntity.getQuery().getParameters().get(p.getName()));
addRelation(pRel);
}
return rightEntity;
}
public void deleteQuery(E entity2Delete) {
if (entity2Delete != null) {
Set<Relation<E>> rels = entity2Delete.getInOutRelations();
if (rels != null) {
rels.stream().forEach((rel) -> {
removeRelation(rel);
});
}
removeEntity((E) entity2Delete);
}
}
public void addReferenceRelation(ReferenceRelation<E> aRelation) {
referenceRelations.add(aRelation);
fireRelationAdded(aRelation);
}
public void removeReferenceRelation(ReferenceRelation<E> aRelation) {
referenceRelations.remove(aRelation);
fireRelationRemoved(aRelation);
}
public Set<ReferenceRelation<E>> getReferenceRelations() {
return referenceRelations;
}
public Set<ReferenceRelation<E>> getReferenceRelationsByEntity(E aEntity) {
Set<ReferenceRelation<E>> res = new HashSet<>();
referenceRelations.stream().forEach((ReferenceRelation<E> rel) -> {
if (rel.getLeftEntity() == aEntity || rel.getRightEntity() == aEntity) {
res.add(rel);
}
});
return res;
}
}