package org.activityinfo.legacy.shared.adapter;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.*;
import org.activityinfo.core.client.InstanceQuery;
import org.activityinfo.core.shared.Projection;
import org.activityinfo.core.shared.application.ApplicationProperties;
import org.activityinfo.core.shared.criteria.Criteria;
import org.activityinfo.core.shared.criteria.IdCriteria;
import org.activityinfo.legacy.shared.model.ActivityFormDTO;
import org.activityinfo.model.form.FormInstance;
import org.activityinfo.legacy.client.Dispatcher;
import org.activityinfo.legacy.shared.adapter.projection.LocationProjector;
import org.activityinfo.legacy.shared.adapter.projection.SiteProjector;
import org.activityinfo.legacy.shared.command.*;
import org.activityinfo.legacy.shared.command.result.SiteResult;
import org.activityinfo.legacy.shared.model.SchemaDTO;
import org.activityinfo.model.form.FormClass;
import org.activityinfo.model.form.FormField;
import org.activityinfo.model.formTree.FieldPath;
import org.activityinfo.model.legacy.CuidAdapter;
import org.activityinfo.model.resource.ResourceId;
import org.activityinfo.model.type.FieldValue;
import org.activityinfo.model.type.ReferenceValue;
import org.activityinfo.promise.BiFunction;
import org.activityinfo.promise.Promise;
import javax.annotation.Nullable;
import java.util.*;
/**
* Naive implementation that joins and projects a multi-level
* instance query.
*/
class Joiner implements Function<InstanceQuery, Promise<List<Projection>>> {
private final Criteria criteria;
private final ClassProvider classProvider;
private Set<FieldPath> fields;
private List<FieldPath> joinFields;
private Dispatcher dispatcher;
public Joiner(Dispatcher dispatcher, List<FieldPath> fields, Criteria criteria) {
this.dispatcher = dispatcher;
// find any fields used to do joins
this.joinFields = joinFields(fields);
// create a set of all the fields we need to fetch.
// make sure the join fields are included, otherwise
// we have to maintain a separate structure for them.
this.fields = Sets.newHashSet(fields);
this.fields.addAll(joinFields);
this.criteria = criteria;
this.classProvider = new ClassProvider(dispatcher);
}
private List<FieldPath> joinFields(List<FieldPath> paths) {
// given a set of paths like...
// territory.province.name
// territory.province.code
// territory.name
// attribute.label
//
// we need to find the fields to join on,
// in increasing order of depth:
// territory
// attribute
// territory.province
Set<FieldPath> referenceFields = Sets.newHashSet();
for (FieldPath path : paths) {
if (path.isNested()) {
for (int i = 1; i < path.getDepth(); i++) {
referenceFields.add(path.ancestor(i));
}
}
}
List<FieldPath> ordered = Lists.newArrayList(referenceFields);
Collections.sort(ordered, new Comparator<FieldPath>() {
@Override
public int compare(FieldPath o1, FieldPath o2) {
return o1.getDepth() - o2.getDepth();
}
});
return ordered;
}
@Override
public Promise<List<Projection>> apply(InstanceQuery instanceQuery) {
CriteriaAnalysis criteriaAnalysis = CriteriaAnalysis.analyze(instanceQuery.getCriteria());
if (criteriaAnalysis.isLocationQuery()) {
return projectLocations(criteriaAnalysis, instanceQuery.getFieldPaths());
}
if (criteriaAnalysis.isSiteQuery()) {
return projectSites(criteriaAnalysis, instanceQuery.getFieldPaths());
}
Promise<List<FormInstance>> instances = query(criteria);
Promise<List<FormClass>> classes = instances.join(new FetchFormClasses());
Promise<List<Projection>> results = Promise.fmap(new ProjectFunction(null)).apply(classes, instances);
// now schedule the joins
for (FieldPath fieldToJoin : joinFields) {
results = results.join(new FetchAndJoinFunction(fieldToJoin));
}
// filter
results = results.then(new Function<List<Projection>, List<Projection>>() {
@Nullable @Override
public List<Projection> apply(@Nullable List<Projection> input) {
List<Projection> matching = new ArrayList<Projection>();
for (Projection projection : input) {
if (criteria.apply(projection)) {
matching.add(projection);
}
}
return matching;
}
});
return results;
}
private Promise<List<Projection>> projectSites(CriteriaAnalysis criteriaAnalysis,
final List<FieldPath> fieldPaths) {
ResourceId activityClass = criteriaAnalysis.getClassRestriction();
int activityId = CuidAdapter.getLegacyIdFromCuid(activityClass);
Filter filter = new Filter();
filter.addRestriction(DimensionType.Activity, activityId);
GetSites query = new GetSites();
query.setFilter(filter);
final Promise<ActivityFormDTO> schemaPromise = dispatcher.execute(new GetActivityForm(activityId));
final Promise<SiteResult> sitePromise = dispatcher.execute(query);
return Promise.waitAll(schemaPromise, sitePromise).then(new Supplier<List<Projection>>() {
@Override
public List<Projection> get() {
final ActivityFormDTO schemaDTO = schemaPromise.get();
final SiteProjector siteProjector = new SiteProjector(schemaDTO, criteria, fieldPaths);
return siteProjector.apply(sitePromise.get());
}
});
}
private Promise<List<Projection>> projectLocations(CriteriaAnalysis criteriaAnalysis, List<FieldPath> fieldPaths) {
ResourceId locationTypeClass = criteriaAnalysis.getClassRestriction();
int locationTypeId = CuidAdapter.getLegacyIdFromCuid(locationTypeClass);
GetLocations query = new GetLocations();
query.setLocationTypeId(locationTypeId);
query.setLocationIds(criteriaAnalysis.getIds(CuidAdapter.LOCATION_TYPE_DOMAIN));
return dispatcher.execute(query).then(new LocationProjector(criteria, fieldPaths));
}
private Promise<List<FormInstance>> query(Criteria criteria) {
return new QueryExecutor(dispatcher, criteria).execute();
}
private class ProjectFunction extends BiFunction<List<FormClass>, List<FormInstance>, List<Projection>> {
private FieldPath prefix;
private ProjectFunction(FieldPath prefix) {
this.prefix = prefix;
}
@Override
public List<Projection> apply(List<FormClass> formClasses, List<FormInstance> instances) {
if (prefix != null) {
throw new UnsupportedOperationException();
}
// build a map from property id -> projected field path
Multimap<ResourceId, FieldPath> map = HashMultimap.create();
for (FieldPath path : fields) {
map.put(path.getRoot(), new FieldPath(path.getRoot()));
}
// our map now contains
// _label -> c00001._label
// c23424 -> c00001.c23434
// we now look at super properties to
// additional bindings
for (FormClass formClass : formClasses) {
for (FormField field : formClass.getFields()) {
for (ResourceId superPropertyId : field.getSuperProperties()) {
if (map.containsKey(superPropertyId)) {
map.putAll(field.getId(), map.get(superPropertyId));
}
}
}
}
// now create our projections based on these mappings
List<Projection> projections = Lists.newArrayList();
for (FormInstance instance : instances) {
Projection projection = new Projection(instance.getId(), instance.getClassId());
for (FieldPath classPath : map.get(ApplicationProperties.CLASS_PROPERTY)) {
projection.setValue(classPath, new ReferenceValue(instance.getClassId()));
}
for (Map.Entry<ResourceId, FieldValue> entry : instance.getFieldValueMap().entrySet()) {
for (FieldPath targetPath : map.get(entry.getKey())) {
projection.setValue(targetPath, entry.getValue());
}
}
projections.add(projection);
}
return projections;
}
}
private class FetchFormClasses implements Function<List<FormInstance>, Promise<List<FormClass>>> {
@Override
public Promise<List<FormClass>> apply(List<FormInstance> instances) {
Set<ResourceId> classIds = Sets.newHashSet();
for (FormInstance instance : instances) {
classIds.add(instance.getClassId());
}
return Promise.map(classIds, classProvider);
}
}
/**
* Fetches all instances that are referenced by the parent instances
*/
private class FetchAndJoinFunction implements Function<List<Projection>, Promise<List<Projection>>> {
private FieldPath referenceField;
public FetchAndJoinFunction(FieldPath referenceField) {
this.referenceField = referenceField;
}
@Override
public Promise<List<Projection>> apply(List<Projection> projections) {
// first collect the ids of the nested FormInstances
Set<ResourceId> instanceIds = Sets.newHashSet();
for (Projection projection : projections) {
instanceIds.addAll(projection.getReferenceValue(referenceField));
}
if (instanceIds.isEmpty()) {
return Promise.resolved(projections);
} else {
return new QueryExecutor(dispatcher, new IdCriteria(instanceIds)).execute()
.then(new JoinFunction(referenceField,
projections));
}
}
}
private class JoinFunction implements Function<List<FormInstance>, List<Projection>> {
private final FieldPath referenceField;
private final List<Projection> projections;
public JoinFunction(FieldPath referenceField, List<Projection> projections) {
this.referenceField = referenceField;
this.projections = projections;
}
@Override
public List<Projection> apply(List<FormInstance> instances) {
Map<ResourceId, FormInstance> instanceMap = indexJoinedInstances(instances);
for (Projection projection : projections) {
Set<ResourceId> referencedIds = projection.getReferenceValue(referenceField);
for (ResourceId referencedId : referencedIds) {
FormInstance referenceInstance = instanceMap.get(referencedId);
if (referenceInstance == null) {
throw new IllegalStateException("Missing referenced instance " + referencedId +
" (legacy id: " +
CuidAdapter.getLegacyIdFromCuid(referencedId) + ") for field " +
referenceField);
}
populateReferencedFields(projection, referenceInstance);
}
}
return projections;
}
private Map<ResourceId, FormInstance> indexJoinedInstances(List<FormInstance> instances) {
Map<ResourceId, FormInstance> instanceMap = Maps.newHashMap();
for (FormInstance instance : instances) {
instanceMap.put(instance.getId(), instance);
}
return instanceMap;
}
private void populateReferencedFields(Projection projection, FormInstance referencedInstance) {
for (Map.Entry<ResourceId, FieldValue> entry : referencedInstance.getFieldValueMap().entrySet()) {
FieldPath path = new FieldPath(referenceField, entry.getKey());
if (fields.contains(path)) {
projection.setValue(path, entry.getValue());
}
}
}
}
}