edu.mit.media.funf.config.ConfigRewriteUtil.java Source code

Java tutorial

Introduction

Here is the source code for edu.mit.media.funf.config.ConfigRewriteUtil.java

Source

/**
 * 
 * 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);
            }
        }
    }

}