public class ApiException extends Exception {

    public ApiException(final String message) {
        super(message);
    }
} public class IobeamException extends RuntimeException {

    public IobeamException(final String message) {
        super(message);
    }

    public IobeamException(final ApiException e) {
        this(e.getMessage());
    }
} public class RestException extends ApiException {

    private final StatusCode statusCode;
    private final int error;
    private final String details;

    public RestException(final StatusCode statusCode,
                         final int error,
                         final String message) {
        super(message);
        this.statusCode = statusCode;
        this.error = error;
        this.details = "";
    }

    public RestException(final StatusCode statusCode,
                         final RestError error) {
        super(error.getMessage());
        this.statusCode = statusCode;
        this.error = error.getError();
        this.details = error.getDetails();
    }

    public StatusCode getStatusCode() {
        return statusCode;
    }

    public int getError() {
        return error;
    }

    public String getDetails() {
        return details;
    }
} public interface AuthHandler extends Callable<AuthToken> {

    public void setForceRefresh(boolean force);

    public AuthToken refreshToken() throws IOException, ApiException;
} public class DefaultAuthHandler extends AbstractAuthHandler {

    private static final String FMT_DEFAULT_PATH = "project_%d.authtoken";

    private static Base64Shim.Decoder legacy = Base64Shim.getDecoder();
    private static Base64Shim.Decoder current = Base64Shim.getUrlDecoder();

    public static ProjectBearerAuthToken parseStringToProjectToken(String t) {
        if (t == null) {
            return null;
        }

        int firstDot = t.indexOf('.');
        if (firstDot < 0) {
            return null;
        }

        int secondDot = t.indexOf('.', firstDot + 1);
        String substr = t.substring(firstDot + 1, secondDot);

        byte[] decoded;
        try {
            decoded = current.decode(substr);
        } catch (IllegalArgumentException e) {
            try {
                decoded = legacy.decode(substr);
            } catch (Exception e2) {
                decoded = null;
            }
        }

        if (decoded != null) {
            String s = new String(decoded);
            JSONObject temp = new JSONObject(s);
            temp.put("project_id", temp.getLong("pid"));
            temp.put("token", t);
            temp.put("expires", Util.DATE_FORMAT.format(new Date(temp.getLong("exp") * 1000)));
            try {
                return ProjectBearerAuthToken.fromJson(temp);
            } catch (ParseException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    private RestClient client;
    private final ProjectBearerAuthToken token;

    private DefaultAuthHandler(final RestClient client,
                               final String projectToken,
                               final String storagePath) {
        super(storagePath);
        this.client = client;
        if (client != null) {
            client.setAuthenticationHandler(this);
        }
        token = parseStringToProjectToken(projectToken);
        writeToken(token);
    }

    public DefaultAuthHandler(final RestClient client, final long projectId,
                              final String projectToken, final File storageDir) {
        this(client, projectToken, storageDir == null ? null : new File(
            storageDir, String.format(FMT_DEFAULT_PATH, projectId)).getAbsolutePath());
    }

    public DefaultAuthHandler(final RestClient client, final long projectId,
                              final String projectToken) {
        this(client, projectId, projectToken, null);
    }

    public DefaultAuthHandler(final long projectId, final String projectToken) {
        this(null, projectId, projectToken);
    }

    @Override
    public AuthToken refreshToken() throws IOException, ApiException {
        if (client != null) {
            TokenService service = new TokenService(client);
            TokenService.RefreshProjectToken req = service.refreshProjectToken(token.getToken());
            return req.execute();
        }
        return null;
    }
} @Deprecated
public class DataPoint implements Serializable {

    private static final String KEY_TIME = "time";
    private static final String KEY_VALUE = "value";

    public static final byte TYPE_LONG = 0x0;
    public static final byte TYPE_DOUBLE = TYPE_LONG + 1;
    public static final byte TYPE_STRING = TYPE_DOUBLE + 1;
    /* Byte array format, all start with [time (8)][type (1)] (9 bytes), followed by:
        - for int/long/float/double:     [value (8)] (17 total bytes)
        - for strings:                   [len (4)][value (X)] (13 + X total bytes, X = len of str)
     */

    private final long time;
    private final Object data;


    /**
     * Constructor for a point containing integer data
     *
     * @param time Timestamp of the data point (in milliseconds)
     * @param data Data in 64-bit integer form
     */
    public DataPoint(long time, long data) {
        this.time = time;
        this.data = data;
    }

    /**
     * Constructor for a point containing double data
     *
     * @param time Timestamp of the data point (in milliseconds)
     * @param data Data in double/float form
     */
    public DataPoint(long time, double data) {
        this.time = time;
        this.data = data;
    }

    /**
     * Constructor for a point containing String data
     *
     * @param time Timestamp of the data point (in milliseconds)
     * @param data Data in String form
     */
    public DataPoint(long time, String data) {
        this.time = time;
        this.data = data;
    }

    /**
     * Constructor for a point containing integer data at the current time.
     *
     * @param data Data in 64-bit integer form
     */
    public DataPoint(long data) {
        this(System.currentTimeMillis(), data);
    }

    /**
     * Constructor for a point containing double data at the current time.
     *
     * @param data Data in double/float form
     */
    public DataPoint(double data) {
        this(System.currentTimeMillis(), data);
    }

    /**
     * Constructor for a point containing String data at the current time.
     *
     * @param data Data in String form
     */
    public DataPoint(String data) {
        this(System.currentTimeMillis(), data);
    }

    public long getTime() {
        return time;
    }

    public Object getValue() {
        return data;
    }

    public static DataPoint fromJson(final JSONObject json) throws ParseException {
        long timestamp = json.getLong(KEY_TIME);
        Object value = json.get(KEY_VALUE);
        if (value instanceof Integer || value instanceof Long) {
            return new DataPoint(timestamp, json.getLong(KEY_VALUE));
        } else if (value instanceof Double || value instanceof Float) {
            return new DataPoint(timestamp, json.getDouble(KEY_VALUE));
        } else {
            return new DataPoint(timestamp, json.getString(KEY_VALUE));
        }
    }

    public JSONObject toJson() {
        JSONObject ret = new JSONObject();
        ret.put(KEY_TIME, time);
        ret.put(KEY_VALUE, data);

        return ret;
    }

    public static DataPoint fromByteArray(byte[] array) {
        ByteBuffer buf = ByteBuffer.wrap(array);
        long timestamp = buf.getLong();
        byte type = buf.get();
        if (type == TYPE_LONG) {
            return new DataPoint(timestamp, buf.getLong());
        } else if (type == TYPE_DOUBLE) {
            return new DataPoint(timestamp, buf.getDouble());
        } else if (type == TYPE_STRING) {
            int len = buf.getInt();
            byte[] bytes = new byte[len];
            buf.get(bytes, 0, len);
            return new DataPoint(timestamp, new String(bytes));
        } else {
            // TODO - Throw exception if invalid format.
            return null;
        }
    }

    /**
     * Returns the byte array representation of this data point. Byte array format are as follows:
     *
     * Starts with 8 bytes for time and 1 byte for type for a total of 9 bytes. Then, based on value
     * type, it does the following:
     *
     * (1) int/long/float/double: 8 bytes for the value, for 17 total bytes.
     *
     * (2) strings: 4 bytes for string length, then X bytes for string for a total of 13 + X bytes.
     *
     * @return Byte array representation
     */
    public byte[] toByteArray() {
        int arrSize = 1 + Long.BYTES;  // first byte specifies type of data
        byte type;
        if (data instanceof Long) {
            arrSize += Long.BYTES;
            type = TYPE_LONG;
        } else if (data instanceof Double) {
            arrSize += Double.BYTES;
            type = TYPE_DOUBLE;
        } else {
            arrSize += Integer.BYTES + ((String) data).length();
            type = TYPE_STRING;
        }

        ByteBuffer buf = ByteBuffer.allocate(arrSize);
        buf.putLong(time);
        buf.put(type);
        if (type == TYPE_LONG) {
            buf.putLong((Long) data);
        } else if (type == TYPE_DOUBLE) {
            buf.putDouble((Double) data);
        } else {
            String temp = (String) data;
            buf.putInt(temp.length());
            buf.put(temp.getBytes());
        }
        buf.flip();
        return buf.array();
    }

    @Override
    public int hashCode() {
        return (int) this.time;
    }

    @Override
    public boolean equals(Object object) {
        if (object == null || !(object instanceof DataPoint)) {
            return false;
        }
        DataPoint that = (DataPoint) object;
        return this.time == that.time && this.data.equals(that.data);
    }

    @Override
    public String toString() {
        String dataStr;
        boolean isInt = data instanceof Integer || data instanceof Long;
        boolean isDouble = data instanceof Float || data instanceof Double;
        if (isInt || isDouble) {
            dataStr = KEY_VALUE + "=" + data;
        } else {
            dataStr = KEY_VALUE + "='" + data + "'";
        }
        return "DataPoint{" +
               "time=" + time +
               ", " + dataStr +
               '}';
    }
} public class DataStore implements Serializable {

    public static final class MismatchedLengthException extends RuntimeException {

        public MismatchedLengthException() {
            super("Number of columns and values are not the same.");
        }
    }

    public static final class UnknownFieldException extends RuntimeException {

        public UnknownFieldException(String field) {
            super("Unknown field '" + field + "' trying to be added.");
        }
    }

    public static final class ReservedColumnException extends IllegalArgumentException {

        public ReservedColumnException(String column) {
            super("'" + column + "' is a reserved column name.");
        }
    }

    private static final Logger logger = Logger.getLogger(DataStore.class.getName());
    private static final String KEY_COLUMNS = "fields";
    private static final String KEY_ROWS = "data";
    private static final String[] RESERVED_COLS = {"time", "time_offset", "all"};


    private final TreeSet<String> columns;
    private final TreeMap<Long, Map<String, Object>> rows =
        new TreeMap<Long, Map<String, Object>>();

    /**
     * Constructs a DataStore, using a collection to construct a _set_ of columns. Note: Duplicates
     * will be removed and a warning will be logged.
     *
     * @param columns Set of field names to track in this batch.
     */
    public DataStore(Collection<String> columns) {
        checkColumns(columns);
        this.columns = new TreeSet<String>(columns);
        if (columns.size() != this.columns.size()) {
            logger.warning("Size mismatch in provided list of columns and resulting set of " +
                           "columns; list may have contained duplicates.");
        }
    }

    public DataStore(String... columns) {
        this(Arrays.asList(columns));
    }

    private void checkColumns(Collection<String> columns) {
        for (String c : columns) {
            if (c == null || c.isEmpty()) {
                throw new IllegalArgumentException("Column cannot be null or empty string.");
            }
            if (Arrays.asList(RESERVED_COLS).contains(c.toLowerCase())) {
                throw new ReservedColumnException(c);
            }
        }
    }

    /**
     * Add a data row, consisting of one column, to the store at a particular time.
     *
     * @param timestamp Timestamp for the data point.
     * @param column    The column for a field to value mapping
     * @param value     The value for a field to value mapping
     */
    public void add(long timestamp, String column, Object value) {
        if (!(value instanceof Long) && !(value instanceof Integer) && !(value instanceof Double) &&
            !(value instanceof Float) && !(value instanceof Boolean) &&
            !(value instanceof String)) {
            throw new IllegalArgumentException(
                "value must be of type: Long, Integer, Double, Float, Boolean, or String");
        }
        Map<String, Object> temp = new HashMap<String, Object>();
        temp.put(column, value);
        add(timestamp, temp);
    }

    /**
     * Add a data row, consisting of one column, to the store at the current time.
     *
     * See {@link #add(long, Map)} for more information.
     *
     * @param column The column for a field to value mapping
     * @param value  The value for a field to value mapping
     */
    public void add(String column, Object value) {
        add(System.currentTimeMillis(), column, value);
    }

    /**
     * Add a data row to the batch at a particular timestamp.
     *
     * This method will throw a `MismatchedLengthException` if the length of `columns` and `values`
     * are not the same.
     *
     * See {@link #add(long, Map)} for more information.
     *
     * @param timestamp Timestamp for all data points
     * @param columns   The list of columns for a field to value mapping
     * @param values    The list of values for a field to value mapping
     */
    public void add(long timestamp, String[] columns, Object[] values) {
        add(timestamp, Arrays.asList(columns), Arrays.asList(values));
    }

    /**
     * Add a data row to the batch with the current time.
     *
     * This method will throw a `MismatchedLengthException` if the length of `columns` and `values`
     * are not the same.
     *
     * See {@link #add(long, Map)} for more information.
     *
     * @param columns The list of columns for a field to value mapping
     * @param values  The list of values for a field to value mapping
     */
    public void add(String[] columns, Object[] values) {
        add(System.currentTimeMillis(), columns, values);
    }

    /**
     * Add a data row to the batch at a particular timestamp.
     *
     * This method will throw a `MismatchedLengthException` if the size of `columns` and `values`
     * are not the same.
     *
     * See {@link #add(long, Map)} for more information.
     *
     * @param timestamp Timestamp for all data points
     * @param columns   The list of columns for a field to value mapping
     * @param values    The list of values for a field to value mapping
     */
    public void add(long timestamp, List<String> columns, List<Object> values) {
        if (columns.size() != values.size()) {
            throw new MismatchedLengthException();
        }
        Map<String, Object> temp = new HashMap<String, Object>();
        for (int i = 0; i < columns.size(); i++) {
            temp.put(columns.get(i), values.get(i));
        }
        add(timestamp, temp);
    }

    /**
     * Add a data row to the batch with the current time.
     *
     * This method will throw a `MismatchedLengthException` if the size of `columns` and `values`
     * are not the same.
     *
     * See {@link #add(long, Map)} for more information.
     *
     * @param columns The list of columns for a field to value mapping
     * @param values  The list of values for a field to value mapping
     */
    public void add(List<String> columns, List<Object> values) {
        add(System.currentTimeMillis(), columns, values);
    }

    /**
     * Add a data row to the batch at a particular timestamp, merging with previous values if
     * needed.
     *
     * This method will throw an `UnknownFieldException` if `data` contains a key that is not in the
     * set of columns this batch was constructed with.
     *
     * @param timestamp Timestamp for all data points
     * @param data      Map that has field names as keys and the data value as values.
     */
    public void add(long timestamp, Map<String, Object> data) {
        for (String k : data.keySet()) {
            if (!columns.contains(k)) {
                throw new UnknownFieldException(k);
            }
        }

        Map<String, Object> curr = this.rows.get(timestamp);
        if (curr == null) {
            this.rows.put(timestamp, new HashMap<String, Object>(data));
        } else {
            curr.putAll(data);
        }
    }

    /**
     * Add a data row to the batch with the current time.
     *
     * This method will throw an `UnknownFieldException` if `data` contains a key that is not in the
     * set of columns this batch was constructed with.
     *
     * @param data Map that has field names as keys and the data value as values.
     */
    public void add(Map<String, Object> data) {
        add(System.currentTimeMillis(), data);
    }

    /**
     * Add the entirety of another DataStore into this one.
     *
     * @param other The other DataStore to merge in
     */
    public void merge(DataStore other) {
        if (!this.hasSameColumns(other)) {
            throw new IllegalArgumentException("DataStore must have the same columns to merge");
        }

        this.rows.putAll(other.rows);
    }

    /**
     * Return a list of the field names tracked by this batch.
     *
     * @return List that is a copy of the columns tracked by this batch.
     */
    public List<String> getColumns() {
        return new ArrayList<String>(this.columns);
    }

    /**
     * Return the rows of this batch as a Map from time to a Map from column to value.
     *
     * @return Map from a time to a Map from column o value.
     */
    public TreeMap<Long, Map<String, Object>> getRows() {
        return new TreeMap<Long, Map<String, Object>>(this.rows);
    }

    /**
     * The number of data values currently stored in this batch, i.e., the product of the number of
     * rows times the number of columns. (Empty data values are counted).
     *
     * @return Size of this DataStore
     */
    public long getDataSize() {
        return this.rows.size() * this.columns.size();
    }

    /**
     * Check if another DataStore has the same columns as this one (weak equality check).
     *
     * @param other Other DataStore to compare
     * @return True if the set of columns are the same
     */
    public boolean hasSameColumns(DataStore other) {
        return other != null && this.columns.equals(other.columns);
    }

    /**
     * Check if this DataStore has the given columns.
     *
     * @param columns Columns to check for
     * @return True if the column sets are equal
     */
    public boolean hasColumns(Collection<String> columns) {
        return columns != null && this.columns.equals(new TreeSet<String>(columns));
    }

    /**
     * Create a DataStore object from JSON.
     *
     * @param json JSON of the DataStore
     * @return DataStore object corresponding to the given JSON
     * @throws ParseException If the JSON is invalid
     */
    public static DataStore fromJson(final JSONObject json) throws ParseException {
        DataStore ret;

        JSONArray jsonCols = json.getJSONArray("fields");
        if (!"time".equals(jsonCols.get(0))) {
            throw new JSONException("time must be the first item in 'fields'");
        }

        Set<String> cols = new HashSet<String>();
        for (int i = 1; i < jsonCols.length(); i++) {
            cols.add(jsonCols.getString(i));
        }
        ret = new DataStore(cols);

        JSONArray jsonData = json.getJSONArray("data");
        for (int i = 0; i < jsonData.length(); i++) {
            JSONArray row = jsonData.getJSONArray(i);
            Long ts = row.getLong(0);
            Map<String, Object> vals = new HashMap<String, Object>();
            for (int j = 1; j < row.length(); j++) {
                vals.put(jsonCols.getString(j), row.get(j));
            }
            ret.rows.put(ts, vals);
        }

        return ret;
    }

    /**
     * Convert this batch into a JSON representation.
     *
     * @return JSONObject representing this batch.
     */
    public JSONObject toJson() {
        JSONObject ret = new JSONObject();
        JSONArray columns = new JSONArray(Arrays.asList(new String[]{"time"}));
        for (String f : this.columns) {
            columns.put(f);
        }
        ret.put(KEY_COLUMNS, columns);

        JSONArray data = new JSONArray();
        ret.put(KEY_ROWS, data);
        for (Long ts : rows.keySet()) {
            JSONArray row = new JSONArray();
            row.put(ts);
            Map<String, Object> temp = rows.get(ts);
            for (String f : this.columns) {
                Object val = temp.get(f);
                row.put(val != null ? val : JSONObject.NULL);
            }
            data.put(row);
        }

        return ret;
    }

    @Override
    public String toString() {
        return "DataStore{" +
               "columns=" + this.columns +
               "dataSize=" + this.rows.size() +
               "}";
    }

    public List<DataStore> split(int maxRows) {
        return DataStore.split(this, maxRows);
    }

    /**
     * Alias for `reset()`.
     */
    public void clear() {
        reset();
    }

    /**
     * Removes all the data from the DataStore
     */
    public void reset() {
        this.rows.clear();
    }

    public static List<DataStore> split(DataStore batch, int maxRows) {
        if (maxRows <= 0) {
            throw new IllegalArgumentException("maxRows must be greater than 0");
        }

        List<DataStore> ret = new ArrayList<DataStore>();
        if (batch.rows.size() <= maxRows) {
            ret.add(batch);
        } else {
            for (int i = 0; i < batch.rows.size(); i += maxRows) {
                DataStore temp = new DataStore(batch.columns);
                temp.rows.putAll(batch.rows.subMap((long) i, (long) i + maxRows));
                ret.add(temp);
            }
        }
        return ret;
    }

    public static DataStore snapshot(DataStore batch) {
        DataStore ret = new DataStore(batch.columns);
        ret.rows.putAll(batch.rows);

        return ret;
    }
} public class Device implements Serializable {

    /**
     * A wrapper class around a String that uniquely identifies the device.
     */
    @Deprecated
    public static final class Id implements Serializable {

        private final String id;

        public Id(String id) {
            this.id = id;
        }

        public String getId() {
            return id;
        }

        public static Device.Id fromJson(final JSONObject json)
            throws ParseException {
            return new Device.Id(json.getString("device_id"));
        }

        @Override
        public String toString() {
            return id;
        }
    }

    public static final class Spec {

        private final String id;
        private final String name;
        private final String type;

        public Spec() {
            this(null, null, null);
        }

        public Spec(String id) {
            this(id, null, null);
        }

        public Spec(String id, String name) {
            this(id, name, null);
        }

        public Spec(String id, String name, String type) {
            this.id = id;
            this.name = name;
            this.type = type;
        }
    }

    private final long projectId;
    private final Spec spec;
    private final Date created;

    // TODO(robatticus) Remove in 0.6.0
    @Deprecated
    public Device(String id,
                  long projectId,
                  String name,
                  String type,
                  Date created) {
        this(projectId, new Device.Spec(id, name, type), created);
    }

    // TODO(robatticus) Remove in 0.6.0
    @Deprecated
    public Device(Id id,
                  long projectId,
                  String name,
                  String type,
                  Date created) {
        this(projectId, new Device.Spec(id.getId(), name, type), created);
    }

    public Device(long projectId, Device.Spec spec, Date created) {
        this.projectId = projectId;
        this.spec = spec;
        this.created = created;
    }

    public Device(long projectId, Device.Spec spec) {
        this(projectId, spec, null);
    }

    public Device(Device d) {
        this(d.projectId, d.spec, d.created);
    }

    @JsonProperty("device_id")
    public String getId() {
        return spec.id;
    }

    @JsonProperty("project_id")
    public long getProjectId() {
        return projectId;
    }

    @JsonProperty("device_name")
    public String getName() {
        return spec.name;
    }

    @JsonProperty("device_type")
    public String getType() {
        return spec.type;
    }

    public Date getCreated() {
        return created;
    }

    public static Device fromJson(final JSONObject json) throws ParseException {

        final long projectId = json.getLong("project_id");
        final Date created = Util.parseToDate(json.getString("created"));
        final Builder builder = new Builder(projectId).
            setId(json.getString("device_id")).
            setName(json.optString("device_name", null)).
            setType(json.optString("device_type", null)).
            setCreated(created);

        return builder.build();
    }

    @Override
    public String toString() {
        return "Device{" +
               "id='" + spec.id + "'" +
               ", projectId=" + projectId +
               ", name='" + spec.name + "'" +
               ", type='" + spec.type + "'" +
               ", created=" + (created != null ? Util.DATE_FORMAT.format(created) : null) +
               '}';
    }

    public static class Builder {

        private final long projectId;
        private String deviceId;
        private String deviceName;
        private String deviceType;
        public Date created;

        public Builder(final long projectId) {
            this.projectId = projectId;
        }

        @Deprecated
        public Builder setId(final String id) {
            return this.id(id);
        }

        public Builder id(final String id) {
            this.deviceId = id;
            return this;
        }

        @Deprecated
        public Builder setName(final String name) {
            return this.name(name);
        }

        public Builder name(final String name) {
            this.deviceName = name;
            return this;
        }

        @Deprecated
        public Builder setType(final String type) {
            return this.type(type);
        }

        public Builder type(final String type) {
            this.deviceType = type;
            return this;
        }

        @Deprecated
        public Builder setCreated(final Date created) {
            return this.created(created);
        }

        public Builder created(final Date created) {
            this.created = created;
            return this;
        }

        public Device build() {
            return new Device(projectId, new Spec(deviceId, deviceName, deviceType), created);
        }
    }
} @Deprecated
public class Import implements Serializable {

    private String deviceId;
    private final long projectId;

    /**
     * DataSet is a convenience name for a `HashSet` containing `DataPoint`s.
     */
    @Deprecated
    static final class DataSet extends HashSet<DataPoint> {

        public DataSet() {
            super();
        }

        public DataSet(Set<DataPoint> pts) {
            super(pts);
        }
    }

    @Deprecated
    static final class DataStore extends HashMap<String, DataSet> {

        public DataStore() {
            super();
        }

        public DataStore(Map<String, Set<DataPoint>> store) {
            super(convert(store));
        }

        private static Map<String, DataSet> convert(Map<String, Set<DataPoint>> store) {
            Map<String, DataSet> ret = new HashMap<String, DataSet>();
            for (String k : store.keySet()) {
                ret.put(k, new DataSet(store.get(k)));
            }
            return ret;
        }
    }

    private final DataStore store = new DataStore();

    /**
     * Creates a new Import object for a particular device and project.
     *
     * @param deviceId  Device ID this data is for
     * @param projectId Project ID this data is for
     */
    public Import(String deviceId, long projectId) {
        this.deviceId = deviceId;
        this.projectId = projectId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public long getProjectId() {
        return projectId;
    }

    public long getTotalSize() {
        int total = 0;
        for (DataSet set : store.values()) {
            total += set.size();
        }
        return total;
    }

    /**
     * Gets a mapping from series to an unordered set of DataPoints.
     *
     * @return Mapping from a series name to unordered set of data.
     */
    public Map<String, Set<DataPoint>> getSeries() {
        return new HashMap<String, Set<DataPoint>>(store);
    }

    /**
     * Gets the unordered set of data associated with a series.
     *
     * @param label The name of the series to get
     * @return Set of unordered data for that series.
     */
    public DataSet getDataSet(String label) {
        return store.get(label);
    }

    /**
     * Adds a new data point to a particular series.
     *
     * @param series    The series this data point should be added to. If it doesn't exist, it will
     *                  be created.
     * @param dataPoint Data to be added.
     */
    public void addDataPoint(String series, DataPoint dataPoint) {
        DataSet set = store.get(series);
        if (set == null) {
            set = new DataSet();
            store.put(series, set);
        }
        set.add(dataPoint);
    }

    /**
     * Adds a set of `DataPoint`s to a series.
     *
     * @param series     The series the DataPoints should be added to. If the series doesn't exist,
     *                   it will be created.
     * @param dataPoints Data to be added.
     */
    public void addDataPointSet(String series, Set<DataPoint> dataPoints) {
        DataSet set = store.get(series);
        if (set == null) {
            set = new DataSet(dataPoints);
            store.put(series, set);
        } else {
            set.addAll(dataPoints);
        }
    }

    /**
     * Adds a set of `DataPoint`s to a series.
     *
     * @param series  The series to add the contents of DataSet to. If the series doesn't exist, it
     *                will be created.
     * @param dataSet Data to be added.
     */
    public void addDataSet(String series, DataSet dataSet) {
        addDataPointSet(series, dataSet);
    }

    /**
     * Custom serialization method for this object to JSON.
     *
     * @param out In-out parameter that maps JSON keys to their values.
     * @return A JSONObject representing this object.
     */
    public JSONObject serialize(Map<String, Object> out) {
        JSONObject json = new JSONObject();
        json.put("device_id", deviceId);
        json.put("project_id", projectId);
        out.put("device_id", deviceId);
        out.put("project_id", projectId);
        JSONArray sourcesArr = new JSONArray();
        List<String> sourceNames = new ArrayList<String>(store.keySet());
        Collections.sort(sourceNames);
        for (String k : sourceNames) {
            JSONObject temp = new JSONObject();
            temp.put("name", k);
            JSONArray dataArr = new JSONArray();
            for (DataPoint d : store.get(k)) {
                dataArr.put(d.toJson());
            }
            temp.put("data", dataArr);
            sourcesArr.put(temp);
        }
        json.put("sources", sourcesArr);
        out.put("sources", sourcesArr);
        return json;
    }

    public static Import fromJson(final JSONObject json) throws ParseException {
        return new Import(json.getString("device_id"),
                          json.getLong("project_id"));
    }

    @Override
    public String toString() {
        return "Import{" +
               "deviceId=" + deviceId +
               ", projectId='" + projectId + "'" +
               '}';
    }
} public class ImportBatch implements Serializable {

    private final long projectId;
    private final String deviceId;
    private final DataStore data;
    private final boolean legacy;

    public ImportBatch(long projectId, String deviceId, DataStore data) {
        this(projectId, deviceId, data, false);
    }


    private ImportBatch(long projectId, String deviceId, DataStore data, boolean legacy) {
        this.projectId = projectId;
        this.deviceId = deviceId;
        this.data = data;
        this.legacy = legacy;
    }

    public static ImportBatch createLegacy(long projectId, String deviceId, DataStore data) {
        return new ImportBatch(projectId, deviceId, data, true);
    }

    public long getProjectId() {
        return this.projectId;
    }

    public String getDeviceId() {
        return this.deviceId;
    }

    public DataStore getData() {
        return this.data;
    }

    public boolean isFromLegacy() {
        return this.legacy;
    }

    public JSONObject serialize() {
        JSONObject ret = new JSONObject();
        ret.put("project_id", this.projectId);
        ret.put("device_id", this.deviceId);
        ret.put("sources", this.data.toJson());

        return ret;
    }

    @Deprecated
    public JSONObject serialize(Map<String, Object> out) {
        out.put("project_id", this.projectId);
        out.put("device_id", this.deviceId);
        out.put("sources", this.data.toJson());

        return serialize();
    }

    public JSONObject toJson() {
        return serialize();
    }

    public JSONObject toJson(Map<String, Object> out) {
        return serialize(out);
    }

    @Override
    public String toString() {
        return "ImportBatch{" +
               "projectId=" + this.projectId + ", " +
               "deviceId=" + this.deviceId + ", " +
               "legacy=" + this.legacy + ", " +
               "data=" + this.data +
               "}";
    }
} public class DeviceService {

    public static final int ERR_DUPLICATE_ID = 150;
    private static final String PATH = "/v1/devices";
    private final RestClient client;

    public DeviceService(final RestClient client) {
        this.client = client;
    }

    public final class Get extends RestRequest<DeviceList> {

        protected Get(long projectId) {
            super(client, RequestMethod.GET, PATH,
                  StatusCode.OK, DeviceList.class);
            getBuilder().addParameter("project_id", projectId);
        }

        public Get setOffset(final int offset) {
            getBuilder().addParameter("offset", offset);
            return this;
        }

        public Get setCount(final int count) {
            getBuilder().addParameter("count", count);
            return this;
        }
    }

    /**
     * A Get API request for fetching all the devices associated with a project.
     *
     * @param projectId Id of the project whose devices to fetch.
     * @return A Get API request that can be executed.
     */
    public Get get(long projectId) {
        return new Get(projectId);
    }


    public final class GetDevice extends RestRequest<Device> {

        protected GetDevice(final String deviceId) {
            super(client, RequestMethod.GET,
                  PATH + "/" + deviceId,
                  StatusCode.OK, Device.class);
        }
    }

    /**
     * Retrieves the device info for the corresponding device ID.
     *
     * @param deviceId Id of the device info to fetch.
     * @return A GetDevice API request that can be executed.
     */
    public GetDevice get(final String deviceId) {
        return new GetDevice(deviceId);
    }

    @Deprecated
    public GetDevice get(final Device.Id deviceId) {
        return get(deviceId.getId());
    }

    public final class Add extends RestRequest<Device> {

        protected Add(Device request) {
            super(client, RequestMethod.POST, PATH,
                  ContentType.JSON, request,
                  StatusCode.CREATED, Device.class);
        }
    }

    /**
     * Creates an Add API request for a new Device with the provided data.
     *
     * @param projectId  Project this device belongs to [required].
     * @param deviceId   Desired device ID. If invalid or <tt>null</tt>, a random one will be
     *                   generated.
     * @param deviceName Project-unique name (optional).
     * @param deviceType Device type description (optional).
     * @param created    Creation date for this date; if null, current time will be used.
     * @return An Add API request that can be executed to add the device to the project.
     * @deprecated Use add(long, Device.Spec, Date) instead. Will be removed in next release.
     */
    @Deprecated
    public Add add(long projectId, String deviceId, String deviceName, String deviceType,
                   Date created) {
        return add(projectId, new Device.Spec(deviceId, deviceName, deviceType), created);
    }

    public Add add(final Device device) {
        if (device == null) {
            throw new IllegalArgumentException("device cannot be null");
        }
        return new Add(device);
    }

    public Add add(long projectId, Device.Spec spec, Date created) {
        Date date = created == null ? new Date(System.currentTimeMillis()) : created;
        Device req = new Device(projectId, spec, date);
        return add(req);
    }

    public Add add(long projectId, Device.Spec spec) {
        return add(projectId, spec, null);
    }

    public Add add(long projectId) {
        return add(projectId, null, null);
    }


    public final class Delete extends RestRequest<Void> {

        protected Delete(final String deviceId) {
            super(client, RequestMethod.DELETE,
                  PATH + "/" + deviceId,
                  StatusCode.NO_CONTENT, Void.class);
        }
    }

    /**
     * Creates a Delete API request for a Device to be removed.
     *
     * @param deviceId The device ID of the Device to be removed.
     * @return A Delete API request that can be executed to remove the device.
     */
    public Delete delete(String deviceId) {
        return new Delete(deviceId);
    }

    @Deprecated
    public Delete delete(Device.Id deviceId) {
        return delete(deviceId.getId());
    }
} public class ImportService {

    private final static Logger logger = Logger.getLogger(ImportService.class.getName());
    final static int REQ_MAX_POINTS = 1000;


    private final RestClient client;

    public ImportService(final RestClient client) {
        this.client = client;
    }

    public class Submit extends RestRequest<Void> {

        private static final String PATH = "/v1/imports";

        protected Submit(ImportBatch imp) {
            super(client, RequestMethod.POST, PATH + "/?fmt=table",
                  ContentType.JSON, imp,
                  StatusCode.OK, Void.class);
        }
    }

    private Import cloneImportMetadata(Import imp) {
        return new Import(imp.getDeviceId(), imp.getProjectId());
    }

    public List<Submit> submitBatch(final Import request) {
        List<Submit> ret = new ArrayList<Submit>();

        return ret;
    }

    private List<ImportBatch> convertImportToImportBatchs(Import imp) {
        Map<String, Set<DataPoint>> store = imp.getSeries();
        List<ImportBatch> ret = new ArrayList<ImportBatch>();
        for (String name : store.keySet()) {
            DataStore batch = new DataStore(new String[]{name});
            for (DataPoint p : store.get(name)) {
                batch.add(p.getTime(), new String[]{name}, new Object[]{p.getValue()});
            }
            List<DataStore> batches = batch.split(REQ_MAX_POINTS / batch.getColumns().size());
            for (DataStore b : batches) {
                ret.add(ImportBatch.createLegacy(imp.getProjectId(), imp.getDeviceId(), b));
            }
        }

        return ret;
    }

    private List<ImportBatch> splitBigImportBatches(ImportBatch imp) {
        List<ImportBatch> ret = new ArrayList<ImportBatch>();

        DataStore batch = imp.getData();
        List<DataStore> batches = batch.split(REQ_MAX_POINTS / batch.getColumns().size());
        if (batches.size() == 1) {
            ret.add(imp);
        } else {
            for (DataStore b : batches) {
                ret.add(new ImportBatch(imp.getProjectId(), imp.getDeviceId(), b));
            }
        }
        return ret;
    }

    public List<Submit> submit(final Import request) {
        return submit(request, Collections.<ImportBatch>emptyList());
    }

    public List<Submit> submit(final List<ImportBatch> batches) {
        return submit(null, batches);
    }

    private List<Submit> submit(final Import request, final List<ImportBatch> batches) {
        List<Submit> ret = new ArrayList<Submit>();

        List<ImportBatch> reqs = new ArrayList<ImportBatch>();
        if (request != null) {
            reqs.addAll(convertImportToImportBatchs(request));
        }
        if (batches != null) {
            for (ImportBatch ib : batches) {
                reqs.addAll(splitBigImportBatches(ib));
            }
        }

        for (ImportBatch r : reqs) {
            ret.add(new Submit(r));
        }

        return ret;
    }
}
import com.iobeam.api.ApiException;
import com.iobeam.api.IobeamException;
import com.iobeam.api.RestException;
import com.iobeam.api.auth.AuthHandler;
import com.iobeam.api.auth.DefaultAuthHandler;
import com.iobeam.api.resource.DataPoint;
import com.iobeam.api.resource.DataStore;
import com.iobeam.api.resource.Device;
import com.iobeam.api.resource.Import;
import com.iobeam.api.resource.ImportBatch;
import com.iobeam.api.service.DeviceService;
import com.iobeam.api.service.ImportService;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
package com.iobeam.api.client;



/**
 * The Iobeam client. Handles device registration and importing/sending of data to iobeam.
 */
public class Iobeam {

    private static final Logger logger = Logger.getLogger(Iobeam.class.getName());
    public static final String DEFAULT_API_URL = "https://api.iobeam.com";
    @Deprecated
    public static final String API_URL = DEFAULT_API_URL;
    static final String DEVICE_FILENAME = "iobeam-device-id";

    /**
     * Exception when actions involving the network (registration, sending data) are called before
     * initializing the client.
     */
    public static class NotInitializedException extends ApiException {

        public NotInitializedException() {
            super("iobeam client not initialized.");
        }
    }

    /**
     * Exception for when the device ID could not successfully be saved to disk.
     */
    public static class CouldNotPersistException extends ApiException {

        public CouldNotPersistException() {
            super("iobeam client could not save device id");
        }
    }

    /**
     * SendCallback used when autoRetry is set.
     */
    static final class ReinsertSendCallback extends SendCallback {

        private final SendCallback userCB;
        private final Iobeam client;

        public ReinsertSendCallback(Iobeam iobeam, SendCallback userCB) {
            this.client = iobeam;
            this.userCB = userCB;
        }

        @Override
        public void onSuccess(ImportBatch data) {