package controller;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.ArrayUtils;
import util.Operator;
import db.Database;
import db.DatabaseAccessException;
/**
* The class {@code DataHub} is used to request data from the {@link Database}.
*/
public class DataHub extends Observable implements Observer {
/**
* The {@link Database}, where all elements are stored.
*/
private final Database database;
/**
* The {@link GroupController}, where to get a list of {@link Group}s.
*/
private final GroupController groupController;
/**
* The {@link SubspaceController}, where to get the currently active {@link Subspace}.
*/
private final SubspaceController subspaceController;
/**
* Holds intersections on constraints, for each group.
*/
private HashMap<Integer, HashSet<Integer>> uniqGroupIds = null;
/**
* Holds all groups from the groupcontroller.
*/
private Group[] allGroups = null;
/**
* Union on all active/visible groups.
*/
private HashSet<Integer> uniqIds = null;
/**
* Holds all ids from constraints and is read on a range basis by the worker threads.
*/
private int[] uniqWorkerArray = null;
/**
* Threadpool to speed up the getData() bottleneck.
*/
private ExecutorService xServ = null;
/**
* Number of Threads in the threadpool.
*/
private int numberOfThreads = 0;
/**
* Caches elements, to optimize performance of queries with same subspace/constraints.
*/
private ElementData[] elementCache = null;
/**
* Constructor for a new {@code DataHub}. All parameters may not be {@code null}.
*
* @param database
* the {@link Database}, where the elements are stored.
* @param groupController
* the {@link GroupController}, where to get all active {@link Group}s.
* @param subspaceController
* the {@link SubspaceController}, where to get the active {@link Subspace}.
*/
public DataHub(Database database, GroupController groupController, SubspaceController subspaceController) {
if (database == null || groupController == null || subspaceController == null) {
throw new IllegalArgumentException("database or one controller is null");
}
this.database = database;
// we need to update, if groups changes
this.groupController = groupController;
this.groupController.addObserver(this);
// we need to update, if subspace changes
this.subspaceController = subspaceController;
this.subspaceController.addObserver(this);
int maxIds;
int maxGroups;
try {
maxIds = database.getConnection().createStatement().executeQuery("SELECT COUNT(Id) FROM Objects;")
.getInt(1);
} catch (SQLException e1) {
maxIds = -1;
}
try {
maxGroups = this.groupController.getGroups().length;
} catch (DatabaseAccessException e) {
maxGroups = -1;
}
if (maxIds != -1 && maxGroups != -1) {
// boost performance by specifying capacity and load factor
this.uniqGroupIds = new HashMap<Integer, HashSet<Integer>>((int) (maxGroups * 1.2f), 1.f);
this.uniqIds = new HashSet<Integer>((int) (maxIds * 1.2f), 1.f);
} else {
// no performance boost without load factor and capacity specification
this.uniqGroupIds = new HashMap<Integer, HashSet<Integer>>();
this.uniqIds = new HashSet<Integer>();
}
// initialize threadpool; assume pool size ~ proc+2
this.numberOfThreads = (Runtime.getRuntime().availableProcessors()) + 2;
this.xServ = Executors.newFixedThreadPool(this.numberOfThreads);
}
/**
* Filters selected ids based on active constraints
*
* @throws DatabaseAccessException
* if read operation failed in {@link Database}.
*/
private void evaluateConstraints() throws DatabaseAccessException {
for (Group group : this.allGroups) {
// select constraints accordingly
if (group.isVisible() && group.getConstraints().length > 0) {
Constraint[] allConstraints = group.getConstraints();
// holds sets for each static constraint
ArrayList<HashSet<Integer>> staticConstraintSets = new ArrayList<HashSet<Integer>>();
// holds sets for each dynamic constraint
ArrayList<HashSet<Integer>> dynamicConstraintSets = new ArrayList<HashSet<Integer>>();
// static and dynamic constraints need different evaluation mechanisms
for (Constraint constraint : allConstraints) {
if (constraint instanceof StaticConstraint && constraint.isActive()) {
staticConstraintSets.add(evaluateStaticConstraint((StaticConstraint) constraint));
} else if (constraint instanceof DynamicConstraint && constraint.isActive()) {
dynamicConstraintSets.add(evaluateDynamicConstraint((DynamicConstraint) constraint));
}
}
// intersection on all constraints in a group
HashSet<Integer> intersectionSet = new HashSet<Integer>(
(int) ((staticConstraintSets.size() + dynamicConstraintSets.size()) * 1.2f), 1.f);
// union on all static constraints
for (HashSet<Integer> staticHs : staticConstraintSets) {
intersectionSet.addAll(staticHs);
}
// intersection on all dynamic constraints
for (HashSet<Integer> dynamicHs : dynamicConstraintSets) {
if (intersectionSet.isEmpty()) {
intersectionSet.addAll(dynamicHs);
} else {
intersectionSet.retainAll(dynamicHs);
}
}
this.uniqGroupIds.put(group.getId(), intersectionSet);
}
}
// finally, union on all groups
for (HashSet<Integer> groupSet : this.uniqGroupIds.values()) {
this.uniqIds.addAll(groupSet);
}
}
/**
* Returns unique ids, selected by static constraints
*
* @param staticConstraint static constraints
*
* @return selected, unique ids
*/
private HashSet<Integer> evaluateStaticConstraint(StaticConstraint staticConstraint) {
HashSet<Integer> constraintSet = new HashSet<Integer>(
(int) (staticConstraint.getSelection().length * 1.2f),
1.f);
for (int id : staticConstraint.getSelection()) {
constraintSet.add(id);
}
return constraintSet;
}
/**
* Returns unique ids, selected by dynamic constraints
*
* @param dynamicConstraint dynamic constraints
*
* @return selected, unique ids
*
* @throws DatabaseAccessException
*/
private HashSet<Integer> evaluateDynamicConstraint(DynamicConstraint dynamicConstraint)
throws DatabaseAccessException {
try {
HashSet<Integer> constraintSet = new HashSet<Integer>((int) (this.allGroups.length * 1.2f), 1.f);
Statement stmt = database.getConnection().createStatement();
// select range, WHERE Feature Operator Value, e.g. WHERE "2" > 42.0013
ResultSet rs = stmt.executeQuery(
"SELECT Id FROM Objects WHERE \"" + dynamicConstraint.getFeature().getId() + "\" "
+ operatorToString(dynamicConstraint.getOperator()) + " " + dynamicConstraint.getValue() + ";");
while (rs.next()) {
constraintSet.add(rs.getInt(1));
}
stmt.close();
return constraintSet;
} catch (SQLException e) {
throw new DatabaseAccessException();
}
}
/**
* Converts operator to string accepted by SQL
*
* @param operator operator that should be converted
* @return SQL string
*/
private static String operatorToString(Operator operator) {
switch (operator) {
case EQUAL:
return "=";
case NOT_EQUAL:
// "<>" in some cases of SQL.. go figure
return "!=";
case LESS:
return "<";
case LESS_OR_EQUAL:
return "<=";
case GREATER:
return ">";
case GREATER_OR_EQUAL:
return ">=";
default:
// well .. there isn't a meaningful default, but at least we try not to induce a crash
return "=";
}
}
/**
* This method is used to request data.
*
* When the method is called, it requests the {@link GroupController} and the {@link SubspaceController}, calculates
* the currently active {@link Subspace} and {@link Group} and gets the appropriate {@link Feature}s from the
* {@link Database} or the cache, depending on changes.
*
* @return A sorted list of {@link ElementData}
* @throws DatabaseAccessException
* if read operation failed in {@link Database}.
*/
public synchronized ElementData[] getData() throws DatabaseAccessException {
if (this.elementCache == null) {
// build element cache
try {
this.buildCache();
} catch (InterruptedException e) {
this.elementCache = new ElementData[0];
e.printStackTrace();
}
}
return this.elementCache;
}
/**
* Queries database and fill cache with elements
*
* @return cached elements
* @throws DatabaseAccessException
* if read operation failed in {@link Database}.
* @throws InterruptedException
* if worker threads got interupted
*/
private ElementData[] buildCache() throws DatabaseAccessException, InterruptedException {
// invalidate cache
this.elementCache = null;
// build group cache
this.allGroups = this.groupController.getGroups();
// combine normal features and color features
ArrayList<Feature> combinedFeatures = new ArrayList<Feature>();
combinedFeatures.addAll(Arrays.asList(subspaceController.getActiveSubspace().getFeatures()));
// do not forget to pass the color features
for (Group group : this.allGroups) {
if (group.getColorFeature() != null && !combinedFeatures.contains(group.getColorFeature())) {
combinedFeatures.add(group.getColorFeature());
}
}
// convert
Feature[] features = new Feature[combinedFeatures.size()];
combinedFeatures.toArray(features);
ElementData[] elements = new ElementData[0];
if (features.length > 0) {
String requiredFeatures = buildRequiredFeaturesString(features);
// start evaluating constraints instead of creating unused ElementData objects
this.evaluateConstraints();
int count = this.uniqIds.size();
// no groups availble, get all objects
if (this.allGroups.length == 0 || this.anyGroupSelectsAllObjects()) {
try {
Statement stmt = database.getConnection().createStatement();
// number of rows in the table Objects, needed for array creation
ResultSet rs = stmt.executeQuery("SELECT COUNT(Id) FROM Objects;");
rs.next();
count = rs.getInt(1);
stmt.close();
} catch (SQLException e) {
throw new DatabaseAccessException();
}
} else {
// init worker array
Integer[] integerArray = new Integer[count];
this.uniqIds.toArray(integerArray);
this.uniqWorkerArray = ArrayUtils.toPrimitive(integerArray);
}
elements = new ElementData[count];
String sharedSql = "SELECT " + requiredFeatures + " FROM Objects ";
// adjust number of workers to size of selection
int adjustedNumberOfThreads = (count < this.numberOfThreads) ? count : this.numberOfThreads;
// threaded implementation of bottleneck, for now only prints boundaries
int rowsPerThread = (int) Math.ceil((double) count / adjustedNumberOfThreads);
List<Callable<Object>> jobsQ = new ArrayList<Callable<Object>>(adjustedNumberOfThreads);
for (int i = 0; i < adjustedNumberOfThreads; ++i) {
// let thread i fill elements[start:end]
int start = i * rowsPerThread;
int end = Math.min((i + 1) * rowsPerThread, count);
// fill job queue with specific workload
jobsQ.add(Executors.callable(new DataArrayWorker(this.database, sharedSql, elements,
this.subspaceController.getCalculateEffectiveOutliernessBy(), this.allGroups, features, start,
end, this.uniqWorkerArray, this.uniqGroupIds)));
}
// synchronization: execute and wait on all jobs
this.xServ.invokeAll(jobsQ);
// clean group cache
this.allGroups = null;
}
// clear worker sets / arrays
this.uniqGroupIds.clear();
this.uniqIds.clear();
this.uniqWorkerArray = null;
// fill cache
this.elementCache = elements;
return this.elementCache;
}
/**
* Checks, if the given groups selects all objects
* Any group thats vsible and has no constraints selects all objects
*
* @param group group to check
* @return {@code true} if the group selects all objects, {@code false} otherwise
*/
private static boolean groupSelectsAllObjects(Group group) {
boolean isSelected = false;
if (group.isVisible() && group.getConstraints().length == 0) {
isSelected = true;
}
return isSelected;
}
/**
* Checks, if there is a group that selects all objects
*
* @return {@code true} if there a a group, {@code false} otherwise
*/
private boolean anyGroupSelectsAllObjects() {
boolean selectsAll = false;
for (Group group : this.allGroups) {
if (groupSelectsAllObjects(group)) {
selectsAll = true;
}
}
return selectsAll;
}
/**
* This method builds a String with all required features, to insert into the sql query
*
* @param features
* a list of features
* @return the build string
*/
private static String buildRequiredFeaturesString(Feature[] features) {
// SELECT " + requiredFeatures + " FROM Objects;
// requiredFeatures has to be: "1", "3", "6", ..
StringBuilder strB = new StringBuilder();
for (Feature currentFeature : features) {
strB.append("\"");
strB.append(currentFeature.getId());
strB.append("\"");
strB.append(',');
}
strB.deleteCharAt(strB.lastIndexOf(","));
return strB.toString();
}
@Override
public void update(Observable arg0, Object arg1) {
try {
this.buildCache();
this.setChanged();
this.notifyObservers();
} catch (DatabaseAccessException e) {
// do not notify observers
e.printStackTrace();
} catch (InterruptedException e) {
// do not notify observers
e.printStackTrace();
}
}
}