/**
*
* Funf: Open Sensing Framework
* Copyright (C) 2010-2011 Nadav Aharony, Wei Pan, Alex Pentland.
* Acknowledgments: Alan Gardner
* Contact: nadav@media.mit.edu
*
* Author(s): Pararth Shah (pararthshah717@gmail.com)
*
* This file is part of Funf.
*
* Funf is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Funf is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Funf. If not, see <http://www.gnu.org/licenses/>.
*
*/
package edu.mit.media.funf.config;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import edu.mit.media.funf.FunfManager;
import edu.mit.media.funf.action.Action;
import edu.mit.media.funf.action.ActionAdapter;
import edu.mit.media.funf.action.StartableAction;
import edu.mit.media.funf.action.StartDataSourceAction;
import edu.mit.media.funf.action.StopDataSourceAction;
import edu.mit.media.funf.datasource.CompositeDataSource;
import edu.mit.media.funf.datasource.DataSource;
import edu.mit.media.funf.datasource.ProbeDataSource;
import edu.mit.media.funf.filter.CompositeFilter;
import edu.mit.media.funf.filter.ProbabilisticFilter;
import edu.mit.media.funf.probe.Probe.DataListener;
import edu.mit.media.funf.probe.builtin.AlarmProbe;
/**
* Rewrites the given config JsonObject by transforming the special annotations
* (starting with "@") to expanded json that is directly readable by the GSON
* TypeAdapterFactory code.
*
* See the rewrite() function for details of the config file transformation.
*
*/
public class ConfigRewriteUtil {
public static final String SCHEDULES_FIELD_NAME = "schedules";
public static final String DATA_FIELD_NAME = "data";
public static final String SOURCE_FIELD_NAME = "source";
public static final String DURATION_FIELD_NAME = "duration";
public static final String TARGET_FIELD_NAME = "target";
public static final String FILTER_FIELD_NAME = "filter";
public static final String LISTENER_FIELD_NAME = "listener";
public static final String DELEGATOR_FIELD_NAME = "delegator";
public static final String TYPE = "@type";
public static final String SCHEDULE = "@schedule";
public static final String PROBE = "@probe";
public static final String FILTER = "@filter";
public static final String ACTION = "@action";
public static final String TRIGGER = "@trigger";
public static final String CLASSNAME_PREFIX = FunfManager.class.getPackage().getName();
public static final String PROBE_PREFIX = AlarmProbe.class.getPackage().getName();
public static final String FILTER_PREFIX = ProbabilisticFilter.class.getPackage().getName();
public static final String ACTION_PREFIX = Action.class.getPackage().getName();
public static final String DATA_LISTENER = DataListener.class.getName();
public static final String DATA_SOURCE = DataSource.class.getName();
public static final String PROBE_DS = ProbeDataSource.class.getName();
public static final String COMPOSITE_DS = CompositeDataSource.class.getName();
public static final String ALARM_PROBE = AlarmProbe.class.getName();
public static final String ACTION_ADAPTER = ActionAdapter.class.getName();
public static final String STARTABLE_ACTION = StartableAction.class.getName();
public static final String START_DS_ACTION = StartDataSourceAction.class.getName();
public static final String STOP_DS_ACTION = StopDataSourceAction.class.getName();
public static final String COMPOSITE_FILTER = CompositeFilter.class.getName();
public static JsonParser parser = new JsonParser();
/**
* Rewrite the given config json. The config can be specified in a compact
* format using annotations (see wiki for details). This function expands
* each annotation to obtain a config file format that is easily parsable
* by gson TypeAdapters.
*
* The following annotations are replaced recursively (one at a time, and
* strictly in this order):
*
* "@schedule", "@filters", "@actions", "@probe"
*
* To see the details of how each annotation is rewritten, see the specific
* functions, for eg. rewriteScheduleAnnotation().
*
* @param base
*/
public static void rewrite(JsonObject base) {
recursiveReplace(base, SCHEDULE);
recursiveReplace(base, FILTER);
recursiveReplace(base, ACTION);
recursiveReplace(base, PROBE);
}
/**
* Performs a recursive check for the given annotation in the given base object.
*
* If an object is found to contain the annotation, the appropriate rewrite
* function is called on that object, by using rewriteFnSelector().
*
* In case of nested annotations, the innermost ones will be replaced first,
* followed by outer ones.
*
* @param base
* @param annotation
*/
public static void recursiveReplace(JsonObject base, String annotation) {
if (base == null) {
return;
}
// Due to the difficulty in doing in-place modification of JsonObject or
// JsonArray, the recursive search is performed in such a way that
// a reference to the parent object of the JsonObject containing
// the given annotation is always available.
// If such a JsonObject is found, it is passed to the appropriate rewrite
// function, which returns a new transformed JsonObject.
// The old object is replaced by the new one by modifying the reference to
// it in the parent JsonObject.
// In case of JsonArray, we cannot modify individual elements, so a new array
// is created from scratch, and the old array is replaced by it.
for (Map.Entry<String, JsonElement> entry: base.entrySet()) {
if (entry.getValue().isJsonArray()) {
JsonArray newArray = new JsonArray();
for (JsonElement arrayEl: entry.getValue().getAsJsonArray()) {
JsonElement elToAdd = arrayEl;
if (arrayEl.isJsonObject()) {
JsonObject arrayObj = arrayEl.getAsJsonObject();
recursiveReplace(arrayObj, annotation);
if (arrayObj.has(annotation)) {
JsonObject newObj = rewriteFnSelector(annotation, arrayObj);
if (newObj != null) {
elToAdd = newObj;
}
}
}
newArray.add(elToAdd);
}
entry.setValue(newArray);
} else if (entry.getValue().isJsonObject()) {
JsonObject entryObj = entry.getValue().getAsJsonObject();
recursiveReplace(entryObj, annotation);
if (entryObj.has(annotation)) {
JsonObject newObj = rewriteFnSelector(annotation, entryObj);
if (newObj != null)
entry.setValue(newObj);
}
}
}
}
public static JsonObject rewriteFnSelector(String annotation, JsonObject baseObj) {
if (annotation != null && baseObj != null) {
if (SCHEDULE.equals(annotation)) {
return rewriteScheduleAnnotation(baseObj);
} else if (FILTER.equals(annotation)) {
return rewriteFiltersAnnotation(baseObj);
} else if (ACTION.equals(annotation)) {
return rewriteActionAnnotation(baseObj);
} else if (PROBE.equals(annotation)) {
return rewriteProbeAnnotation(baseObj);
}
}
return null;
}
/**
* If the given JsonObject contains a member named "@schedule", returns
* a CompositeDataSource.
*
* Renames the "strict" property in the schedule object to "exact" in
* the AlarmProbe.
*
* eg.
* { "@probe": ".AccelerometerSensorProbe",
* "sensorDelay": "MEDIUM",
* "@schedule": { "@probe": ".ActivityProbe",
* "sensorDelay": "FASTEST",
* "@schedule": { "interval": 60, "offset": 12345 },
* "@filters": [
* { "@type": ".KeyValueFilter",
* "matches": { "motionState": "Driving" } },
* { "@type": ".ProbabilityFilter", "probability": 0.5 }
* ] }
* }
*
* will be rewritten as:
*
* { "@type": "edu.mit.media.fun.datasource.CompositeDataSource",
* "source": { "@type": "edu.mit.media.fun.datasource.CompositeDataSource",
* "source": { "@probe": ".AlarmProbe", "interval": 60, "offset": 12345 },
* "@action": { "@type": ".StartDataSourceAction",
* "delegate": { "@probe": ".ActivityProbe",
* "sensorDelay": "FASTEST",
* "@filters": [
* { "@type": ".KeyValueFilter",
* "matches": { "motionState": "Driving" } },
* { "@type": ".ProbabilityFilter",
* "probability": 0.5 }
* ] } } }
* "@action": { "@type: ".StartDataSourceAction",
* "delegate": { "@probe": ".AccelerometerSensorProbe",
* "sensorDelay": "MEDIUM" } } }
*
* @param baseObj The JsonObject which contains a member named "@schedule"
*/
public static JsonObject rewriteScheduleAnnotation(JsonObject baseObj) {
if (baseObj == null)
return null;
// The CompositeDataSource which will be returned from this rewrite.
JsonObject dataSourceObj = new JsonObject();
dataSourceObj.addProperty(TYPE, COMPOSITE_DS);
JsonObject scheduleObj = (JsonObject)baseObj.remove(SCHEDULE);
// If scheduleObj is already a CompositeDataSource, it implies that
// there was a nested @schedule annotation which has already been
// taken care of by an earlier call to this function. In that case
// the scheduleObj will simply be taken as a nested "source" of
// the current CompositeDataSource.
if (!isDataSourceObject(scheduleObj)) {
// If it is not a data source, then the "@probe" annotation in the
// scheduleObj will be parsed as a data source by rewriteProbeAnnotation().
// For compatibility, "@type" annotations indicating probes are
// converted to "@probe" annotations.
// For compatibility, if neither "@type" or "@probe" annotations
// exist, an "AlarmProbe" is added (as that signifies the default behavior
// of the earlier scheduling system).
if (scheduleObj.has(TYPE)) {
renameJsonObjectKey(scheduleObj, TYPE, PROBE);
} else if (!scheduleObj.has(PROBE)){
scheduleObj.addProperty(PROBE, ALARM_PROBE);
}
}
Double duration = 0.0;
if (scheduleObj.has(PROBE) &&
ALARM_PROBE.equals(scheduleObj.get(PROBE).getAsString())) {
renameJsonObjectKey(scheduleObj, "strict", "exact");
if (scheduleObj.has(DURATION_FIELD_NAME))
duration = scheduleObj.remove(DURATION_FIELD_NAME).getAsDouble();
}
JsonElement filtersEl = null;
if (scheduleObj.has(FILTER)) {
filtersEl = scheduleObj.remove(FILTER);
}
// If baseObj is itself a schedule object (i.e this is a nested schedule),
// a "@trigger" annotation would provide the action to be performed by
// the outer schedule object. This must be kept separate from other members
// of baseObj.
JsonObject triggerObj = null;
if (baseObj.has(TRIGGER)) {
triggerObj = baseObj.remove(TRIGGER).getAsJsonObject();
}
// To select the action to be performed whenever this schedule object fires:
// 1. First check if the baseObj is really a probe or a data source. If
// it is empty except for the "@schedule" tag, then don't register any action.
// (Happens in the direct "schedules" member.)
// 2. If it is not empty, then check if a "@trigger" annotation exists, which denotes
// a user-specified custom action to be registered whenever scheduler fires.
// 3. If no "@trigger" exists, then select a default Action. If "duration" was
// specified in the schedule object, add a StartableAction to run
// the dependent data source for that duration.
// 4. If no non-zero duration was specified, add a StartDataSourceAction to simply start
// the dependent data source whenever this schedule object fires.
JsonObject actionObj = null;
if (baseObj.has(PROBE) || baseObj.has(TYPE)) {
if (!isDataSourceObject(baseObj)) {
renameJsonObjectKey(baseObj, TYPE, PROBE);
}
if (scheduleObj.has(TRIGGER)) {
actionObj = scheduleObj.remove(TRIGGER).getAsJsonObject();
} else {
actionObj= new JsonObject();
if (duration > 0) {
actionObj.addProperty(TYPE, STARTABLE_ACTION);
actionObj.addProperty(DURATION_FIELD_NAME, duration);
} else {
actionObj.addProperty(TYPE, START_DS_ACTION);
}
}
actionObj.add(TARGET_FIELD_NAME, baseObj);
}
dataSourceObj.add(SOURCE_FIELD_NAME, scheduleObj);
if (filtersEl != null)
dataSourceObj.add(FILTER, filtersEl);
if (actionObj != null)
dataSourceObj.add(ACTION, actionObj);
if (triggerObj != null)
dataSourceObj.add(TRIGGER, triggerObj);
return dataSourceObj;
}
/**
* Rewrites the filter array denoted by "@filters" to a "filters" member
* with nested filters, in the order of their appearance in the array.
*
* If the baseObj is not a CompositeDataSource, converts it into one, and
* pushing the "@probe" annotation (if it exists) to the "source" field.
*
* If the entire filter class name is not specified, it will be prefixed by
* FILTER_PREFIX.
*
* eg.
* { "@probe": ".ActivityProbe",
* "sensorDelay": "FASTEST",
* "@filters": [{ "@type": ".KeyValueFilter", "matches": { "motionState": "Driving" } },
* { "@type": ".ProbabilityFilter", "probability": 0.5 } ]
* }
*
* will be rewritten to
*
* { "@type": "edu.mit.media.funf.datasource.CompositeDataSource",
* "source": { "@probe": ".ActivityProbe", "sensorDelay": "FASTEST" },
* "filters": { "@type": "edu.mit.media.funf.filter.KeyValueFilter",
* "matches": { "motionState": "Driving" }
* "listener": { "@type": "edu.mit.media.funf.filter.ProbabilityFilter",
* "probability": 0.5 } }
* }
*
* @param baseObj
*/
public static JsonObject rewriteFiltersAnnotation(JsonObject baseObj) {
if (baseObj == null)
return null;
JsonElement filterEl = baseObj.remove(FILTER);
JsonObject filterObj = null;
if (filterEl.isJsonArray()) {
for (JsonElement filterIter: filterEl.getAsJsonArray()) {
if (filterIter.isJsonObject()) {
// Add the filter class name prefix if not specified.
addTypePrefix(filterIter.getAsJsonObject(), FILTER_PREFIX);
}
}
filterObj = new JsonObject();
filterObj.addProperty(TYPE, COMPOSITE_FILTER);
filterObj.add("filters", filterEl.getAsJsonArray());
} else {
filterObj = filterEl.getAsJsonObject();
}
// Add the filter class name prefix if not specified.
addTypePrefix(filterObj.getAsJsonObject(), FILTER_PREFIX);
// Insert the filter object denoted by "@filter" to the existing
// "filter" field.
insertFilter(baseObj, filterObj);
// If the baseObj is not a data source, convert it into CompositeDataSource.
if (!isDataSourceObject(baseObj)) {
JsonObject dataSourceObj = new JsonObject();
dataSourceObj.addProperty(TYPE, COMPOSITE_DS);
dataSourceObj.add(FILTER_FIELD_NAME, baseObj.remove(FILTER_FIELD_NAME));
if (baseObj.has(ACTION)) {
dataSourceObj.add(ACTION, baseObj.remove(ACTION));
}
// The remaining fields of baseObj should be "@probe" annotation and
// probe parameters.
dataSourceObj.add(SOURCE_FIELD_NAME, baseObj);
return dataSourceObj;
} else {
return baseObj;
}
}
/**
* Rewrites "@action" annotation as an Action object, and adds it to the
* end of the "filter" chain.
*
* If the specified Action does not implement the DataListener interface,
* it is wrapped by an ActionAdapter.
*
* If the entire action class name is not specified, it will be prefixed by
* ACTION_PREFIX.
*
* @param baseObj
*/
public static JsonObject rewriteActionAnnotation(JsonObject baseObj) {
if (baseObj == null)
return null;
// Add prefix if entire class name is not specified.
JsonObject actionObj = baseObj.remove(ACTION).getAsJsonObject();
addTypePrefix(actionObj, ACTION_PREFIX);
// Check if the specified Action type implements the DataListener
// interface.
String actionType = actionObj.get(TYPE).getAsString();
boolean isDataListener = false;
try {
Class<?> runtimeClass = Class.forName(actionType);
for (Class<?> runtimeInterface: runtimeClass.getInterfaces()) {
if (DATA_LISTENER.equals(runtimeInterface.getName())) {
isDataListener = true;
break;
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// Wrap the Action with an ActionAdapter if it does not
// implement the DataListener interface and insert the
// Action to the end of the "filter" chain.
if (!isDataListener) {
JsonObject actionAdapter = new JsonObject();
actionAdapter.addProperty(TYPE, ACTION_ADAPTER);
actionAdapter.add(TARGET_FIELD_NAME, actionObj);
insertFilter(baseObj, actionAdapter);
} else {
insertFilter(baseObj, actionObj);
}
// If the baseObj is not a data source, convert it into CompositeDataSource.
if (!isDataSourceObject(baseObj)) {
JsonObject dataSourceObj = new JsonObject();
dataSourceObj.addProperty(TYPE, COMPOSITE_DS);
dataSourceObj.add(FILTER_FIELD_NAME, baseObj.remove(FILTER_FIELD_NAME));
// The remaining fields of baseObj should be "@probe" annotation and
// probe parameters.
dataSourceObj.add(SOURCE_FIELD_NAME, baseObj);
return dataSourceObj;
} else {
return baseObj;
}
}
/**
* If the given JsonObject contains a member named "@probe", rewrite the object
* as a ProbeDataSource consisting of the given probe. The remaining members of
* the baseObj should be parameters of the specified probe.
*
* If the entire probe class name is not specified, it will be prefixed by
* PROBE_PREFIX.
*
* eg.
* { "@probe": ".AlarmProbe", "interval": 300, "exact": false, "offset": 12345 }
*
* will be rewritten to
*
* { "@type": "edu.mit.media.funf.datasource.ProbeDataSource",
* "source": { "@probe": "edu.mit.media.funf.probe.builtin.AlarmProbe",
* "interval": 300, "exact": false, "offset": 12345 } }
*
* @param baseObj
*/
public static JsonObject rewriteProbeAnnotation(JsonObject baseObj) {
if (baseObj == null)
return null;
JsonObject dataSourceObj = new JsonObject();
dataSourceObj.addProperty(TYPE, PROBE_DS);
renameJsonObjectKey(baseObj, PROBE, TYPE);
addTypePrefix(baseObj, PROBE_PREFIX);
dataSourceObj.add(SOURCE_FIELD_NAME, baseObj);
return dataSourceObj;
}
public static void renameJsonObjectKey(JsonObject object, String currentKey, String newKey) {
if (object.has(currentKey)) {
object.add(newKey, object.get(currentKey));
object.remove(currentKey);
}
}
/**
* Check if the "@type" member of the given object is one of PROBE_DS
* or COMPOSITE_DS.
*
* @param object
* @return
*/
public static boolean isDataSourceObject(JsonObject object) {
if (object.has(TYPE)) {
String type = object.get(TYPE).getAsString();
try {
Class<?> runtimeClass = Class.forName(type);
return implementsInterface(runtimeClass, DATA_SOURCE);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return false;
}
private static boolean implementsInterface(Class<?> runtimeClass, String interfaceName) {
if (runtimeClass == null)
return false;
for (Class<?> runtimeInterface: runtimeClass.getInterfaces()) {
if (interfaceName.equals(runtimeInterface.getName())) {
return true;
}
}
Class<?> parentClass = runtimeClass.getSuperclass();
return implementsInterface(parentClass, interfaceName);
}
/**
* Insert the given filter object to the end of the filters chain in
* the "filter" member of given baseObj.
*
* The array of filters is converted into nested filters, with each
* subsequent filter added as a listener of the previous filter, in the
* order of presence in the given array.
*
* @param baseObj
* @param filters
*/
public static void insertFilter(JsonObject baseObj, JsonObject filter) {
// If the "filter" field already exists in the baseObj, iterate to the
// end of the chain and add the newFilters object.
if (baseObj.has(FILTER_FIELD_NAME)) {
JsonObject currFilters = baseObj.remove(FILTER_FIELD_NAME).getAsJsonObject();
JsonObject iterFilter = currFilters;
while (iterFilter.has(LISTENER_FIELD_NAME)) {
iterFilter = currFilters.get(LISTENER_FIELD_NAME).getAsJsonObject();
}
iterFilter.add(LISTENER_FIELD_NAME, filter);
baseObj.add(FILTER_FIELD_NAME, currFilters);
} else {
baseObj.add(FILTER_FIELD_NAME, filter);
}
}
/**
* If the "@type" member of the given object is an incomplete type,
* i.e. it starts with a ".", then add the given prefix to that type.
*
* @param object
* @param prefix
*/
public static void addTypePrefix(JsonObject object, String prefix) {
if (object.has(TYPE)) {
String type = object.get(TYPE).getAsString();
if (type.startsWith(".")) {
type = prefix + type;
object.addProperty(TYPE, type);
}
}
}
}