com.stackmob.sdk.model.StackMobModel.java Source code

Java tutorial

Introduction

Here is the source code for com.stackmob.sdk.model.StackMobModel.java

Source

/**
 * Copyright 2012 StackMob
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.stackmob.sdk.model;

import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.stackmob.sdk.api.StackMob;
import com.stackmob.sdk.callback.StackMobCallback;
import com.stackmob.sdk.callback.StackMobIntermediaryCallback;
import com.stackmob.sdk.callback.StackMobNoopCallback;
import com.stackmob.sdk.exception.StackMobException;
import com.stackmob.sdk.util.Pair;
import com.stackmob.sdk.util.RelationMapping;
import com.stackmob.sdk.util.SerializationMetadata;

import static com.stackmob.sdk.util.SerializationMetadata.*;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;

public abstract class StackMobModel {

    public class DateAsNumberTypeAdapter extends TypeAdapter<Date> {

        @Override
        public void write(JsonWriter jsonWriter, Date date) throws IOException {
            if (date == null) {
                jsonWriter.nullValue();
                return;
            }
            jsonWriter.value(date.getTime());
        }

        @Override
        public Date read(JsonReader jsonReader) throws IOException {
            if (jsonReader.peek() == JsonToken.NULL) {
                jsonReader.nextNull();
                return null;
            }
            return new Date(jsonReader.nextLong());
        }
    }

    private transient String id;
    private transient Class<? extends StackMobModel> actualClass;
    private transient String schemaName;
    private transient boolean hasData;
    private transient Gson gson;

    public StackMobModel(String id, Class<? extends StackMobModel> actualClass) {
        this(actualClass);
        this.id = id;
    }

    public StackMobModel(Class<? extends StackMobModel> actualClass) {
        this.actualClass = actualClass;
        schemaName = actualClass.getSimpleName().toLowerCase();
        ensureValidName(schemaName, "model");
        ensureMetadata(actualClass);
        GsonBuilder gsonBuilder = new GsonBuilder();
        gsonBuilder.registerTypeAdapter(Date.class, new DateAsNumberTypeAdapter());
        gson = gsonBuilder.create();
    }

    private static void ensureValidName(String name, String thing) {
        //The three character minimum isn't actually enforced for fields
        if (name.matches(".*(\\W|_).*") || name.length() > 25 || name.length() < 3) {
            throw new IllegalStateException(
                    String.format("Invalid name for a %s: %s. Must be 3-25 alphanumeric characters", thing, name));
        }
    }

    private SerializationMetadata getMetadata(String fieldName) {
        return getSerializationMetadata(actualClass, fieldName);
    }

    private String getFieldName(String jsonName) {
        return getFieldNameFromJsonName(actualClass, jsonName);
    }

    public void setID(String id) {
        this.id = id;
    }

    public String getID() {
        return id;
    }

    /**
     * Determines the schema connected to this class on the server. By
     * default it's the name of the class in lower case. Override in
     * subclasses to change that. Must be 3-25 alphanumeric characters.
     * @return the schema name
     */
    protected String getSchemaName() {
        return schemaName;
    }

    public String getIDFieldName() {
        return schemaName + "_id";
    }

    public boolean hasData() {
        return hasData;
    }

    protected void fillFieldFromJson(String jsonName, JsonElement json) throws StackMobException {
        try {
            if (jsonName.equals(getIDFieldName())) {
                // The id field is special, its name doesn't match the field
                setID(json.getAsJsonPrimitive().getAsString());
            } else {
                // undo the toLowerCase we do when sending out the json
                String fieldName = getFieldName(jsonName);
                if (fieldName != null) {
                    Field field = getField(fieldName);
                    field.setAccessible(true);
                    if (getMetadata(fieldName) == MODEL) {
                        // Delegate any expanded relations to the appropriate object
                        StackMobModel relatedModel = (StackMobModel) field.get(this);
                        // If there's a model with the same id, keep it. Otherwise create a new one
                        if (relatedModel == null || !relatedModel.hasSameID(json)) {
                            relatedModel = (StackMobModel) field.getType().newInstance();
                        }
                        relatedModel.fillFromJson(json);
                        field.set(this, relatedModel);
                    } else if (getMetadata(fieldName) == MODEL_ARRAY) {
                        Class<? extends StackMobModel> actualModelClass = (Class<? extends StackMobModel>) SerializationMetadata
                                .getComponentClass(field);
                        Collection<StackMobModel> existingModels = getFieldAsCollection(field);
                        List<StackMobModel> newModels = updateModelListFromJson(json.getAsJsonArray(),
                                existingModels, actualModelClass);
                        setFieldFromList(field, newModels, actualModelClass);
                    } else {
                        // Let gson do its thing
                        field.set(this, gson.fromJson(json, field.getType()));
                    }
                }
            }
        } catch (NoSuchFieldException ignore) {
        } catch (IllegalAccessException e) {
            throw new StackMobException(e.getMessage());
        } catch (InstantiationException e) {
            throw new StackMobException(e.getMessage());
        }
    }

    /**
     * Turns a field which is either an Array or Collection of StackMobModels and turns in into a collection
     */
    protected Collection<StackMobModel> getFieldAsCollection(Field field) throws IllegalAccessException {
        if (field.getType().isArray()) {
            // grab the existing collection/array if there is one. We want to reuse any existing objects.
            // Otherwise we might end up clobbering a full object with just an id.
            StackMobModel[] models = (StackMobModel[]) field.get(this);
            return models == null ? null : Arrays.asList(models);
        } else {
            return (Collection<StackMobModel>) field.get(this);
        }
    }

    /**
     * Sets a field which is either an Array or Collection of StackMobModels using a list
     */
    protected void setFieldFromList(Field field, List<? extends StackMobModel> list,
            Class<? extends StackMobModel> modelClass) throws IllegalAccessException, InstantiationException {
        // We want to reuse the existing collection if at all possible
        if (field.getType().isArray()) {
            StackMobModel[] modelArray = (StackMobModel[]) field.get(this);
            if (modelArray == null || modelArray.length != list.size()) {
                field.set(this, Array.newInstance(modelClass, list.size()));
                modelArray = (StackMobModel[]) field.get(this);
            }
            for (int i = 0; i < list.size(); i++) {
                modelArray[i] = list.get(i);
            }
        } else {
            Collection<StackMobModel> models = (Collection<StackMobModel>) field.get(this);
            if (models == null) {
                initWithNewCollection(field);
                models = (Collection<StackMobModel>) field.get(this);
            }
            try {
                models.clear();
            } catch (UnsupportedOperationException e) {
                initWithNewCollection(field);
            }
            models.addAll(list);
        }
    }

    private void initWithNewCollection(Field field) throws IllegalAccessException {
        // Given a null Collection, how to we find the right
        // concrete collection to use? There is no good way.
        // So let's at least use the same hack as gson.
        field.set(this, gson.fromJson("[]", field.getType()));
    }

    protected static List<StackMobModel> updateModelListFromJson(JsonArray array,
            Collection<? extends StackMobModel> existingModels, Class<? extends StackMobModel> modelClass)
            throws IllegalAccessException, InstantiationException, StackMobException {
        List<StackMobModel> result = new ArrayList<StackMobModel>();
        for (JsonElement json : array) {
            StackMobModel model = getExistingModel(existingModels, json);
            if (model == null)
                model = modelClass.newInstance();
            model.fillFromJson(json);
            result.add(model);
        }
        return result;
    }

    /**
     * Finds a model with the same id as the json
     * @param oldList The data in the object already
     * @param json
     * @return
     */
    protected static StackMobModel getExistingModel(Collection<? extends StackMobModel> oldList, JsonElement json) {
        if (oldList != null) {
            //First try to find the existing model in the list
            for (StackMobModel model : oldList) {
                if (model.hasSameID(json)) {
                    return model;
                }
            }
            //If none, pick the first one without an id
            for (StackMobModel model : oldList) {
                if (model.getID() == null) {
                    model.setID(json);
                    return model;
                }
            }
        }
        return null;
    }

    private Field getField(String fieldName) throws NoSuchFieldException {
        Class<?> classToCheck = actualClass;
        while (!classToCheck.equals(StackMobModel.class)) {
            try {
                return classToCheck.getDeclaredField(fieldName);
            } catch (NoSuchFieldException ignored) {
            }
            classToCheck = classToCheck.getSuperclass();
        }
        throw new NoSuchFieldException(fieldName);
    }

    public void fillFromJson(String jsonString) throws StackMobException {
        fillFromJson(new JsonParser().parse(jsonString));
    }

    protected void fillFromJson(JsonElement json) throws StackMobException {
        fillFromJson(json, null);
    }

    protected void fillFromJson(JsonElement json, List<String> selection) throws StackMobException {
        if (json.isJsonPrimitive()) {
            //This ought to be an unexpanded relation then
            setID(json.getAsJsonPrimitive().getAsString());
        } else {
            for (Map.Entry<String, JsonElement> jsonField : json.getAsJsonObject().entrySet()) {
                if (selection == null || selection.contains(jsonField.getKey())) {
                    fillFieldFromJson(jsonField.getKey(), jsonField.getValue());
                }
            }
            hasData = true;
        }
    }

    /**
     * Checks if the current object has the same id as this json
     * @param json
     * @return
     */
    protected boolean hasSameID(JsonElement json) {
        if (getID() == null)
            return false;
        if (json.isJsonPrimitive()) {
            return getID().equals(json.getAsJsonPrimitive().getAsString());
        }
        JsonElement idFromJson = json.getAsJsonObject().get(getIDFieldName());
        return idFromJson != null && getID().equals(idFromJson.getAsString());
    }

    protected void setID(JsonElement json) {
        if (json.isJsonPrimitive()) {
            setID(json.getAsJsonPrimitive().getAsString());
        } else {
            setID(json.getAsJsonObject().get(getIDFieldName()).getAsString());
        }
    }

    private List<String> getFieldNames(JsonObject json) {
        List<String> list = new ArrayList<String>();
        for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
            list.add(entry.getKey());
        }
        return list;
    }

    private JsonElement toJsonElement(int depth, RelationMapping mapping) {
        // Set the id here as opposed to on the server to avoid a race condition
        if (getID() == null)
            setID(UUID.randomUUID().toString().replace("-", ""));
        if (depth < 0)
            return new JsonPrimitive(getID());
        JsonObject json = gson.toJsonTree(this).getAsJsonObject();
        JsonObject outgoing = new JsonObject();
        for (String fieldName : getFieldNames(json)) {
            ensureValidName(fieldName, "field");
            JsonElement value = json.get(fieldName);
            if (getMetadata(fieldName) == MODEL) {
                json.remove(fieldName);
                try {
                    Field relationField = getField(fieldName);
                    relationField.setAccessible(true);
                    StackMobModel relatedModel = (StackMobModel) relationField.get(this);
                    mapping.add(fieldName, relatedModel.getSchemaName());
                    JsonElement relatedJson = relatedModel.toJsonElement(depth - 1, mapping);
                    mapping.leave();
                    if (relatedJson != null)
                        json.add(fieldName, relatedJson);
                } catch (Exception ignore) {
                } //Should never happen
            } else if (getMetadata(fieldName) == MODEL_ARRAY) {
                json.remove(fieldName);
                try {
                    Field relationField = getField(fieldName);
                    relationField.setAccessible(true);
                    JsonArray array = new JsonArray();
                    Collection<StackMobModel> relatedModels;
                    if (relationField.getType().isArray()) {
                        relatedModels = Arrays.asList((StackMobModel[]) relationField.get(this));
                    } else {
                        relatedModels = (Collection<StackMobModel>) relationField.get(this);
                    }
                    boolean first = true;
                    for (StackMobModel relatedModel : relatedModels) {
                        if (first) {
                            mapping.add(fieldName, relatedModel.getSchemaName());
                            first = false;
                        }
                        JsonElement relatedJson = relatedModel.toJsonElement(depth - 1, mapping);
                        if (relatedJson != null)
                            array.add(relatedJson);
                    }
                    if (!first)
                        mapping.leave();
                    json.add(fieldName, array);
                } catch (Exception ignore) {
                } //Should never happen
            } else if (getMetadata(fieldName) == OBJECT) {
                //We don't support subobjects. Gson automatically converts a few types like
                //Date and BigInteger to primitive types, but anything else has to be an error.
                if (value.isJsonObject()) {
                    throw new IllegalStateException(
                            "Field " + fieldName + " is a subobject which is not supported at this time");
                }
            }
            outgoing.add(fieldName.toLowerCase(), json.get(fieldName));
        }
        if (id != null) {
            outgoing.addProperty(getIDFieldName(), id);
        }
        return outgoing;
    }

    public String toJson() {
        return toJsonWithDepth(0);
    }

    public String toJsonWithDepth(int depth) {
        return toJsonWithDepth(depth, new RelationMapping());
    }

    /**
     * Converts the object to JSON turning all Models into their ids
     * @return the json representation of this model
     */
    protected String toJsonWithDepth(int depth, RelationMapping mapping) {
        return toJsonElement(depth, mapping).toString();
    }

    public void fetch() {
        fetch(new StackMobNoopCallback());
    }

    public void load(int depth) {
        fetchWithDepth(depth, new StackMobNoopCallback());
    }

    public void fetch(StackMobCallback callback) {
        fetchWithDepth(0, callback);
    }

    public void fetchWithDepth(int depth, StackMobCallback callback) {
        Map<String, String> args = new HashMap<String, String>();
        if (depth > 0)
            args.put("_expand", String.valueOf(depth));
        Map<String, String> headers = new HashMap<String, String>();
        StackMob.getStackMob().get(getSchemaName() + "/" + id, args, headers,
                new StackMobIntermediaryCallback(callback) {
                    @Override
                    public void success(String responseBody) {
                        try {
                            StackMobModel.this.fillFromJson(new JsonParser().parse(responseBody));
                        } catch (StackMobException e) {
                            failure(e);
                        }
                        super.success(responseBody);
                    }
                });
    }

    public void save() {
        save(new StackMobNoopCallback());
    }

    public void saveWithDepth(int depth) {
        saveWithDepth(depth, new StackMobNoopCallback());
    }

    public void save(StackMobCallback callback) {
        saveWithDepth(0, callback);
    }

    public void saveWithDepth(int depth, StackMobCallback callback) {
        RelationMapping mapping = new RelationMapping();
        String json = toJsonWithDepth(depth, mapping);
        List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>();
        if (!mapping.isEmpty())
            headers.add(new Pair<String, String>("X-StackMob-Relations", mapping.toHeaderString()));
        StackMob.getStackMob().post(getSchemaName(), json, headers, new StackMobIntermediaryCallback(callback) {
            @Override
            public void success(String responseBody) {
                try {
                    fillFromJson(new JsonParser().parse(responseBody), Arrays.asList("lastmoddate", "createddate"));
                } catch (StackMobException e) {
                    failure(e);
                }
                super.success(responseBody);
            }
        });
    }

    public void destroy() {
        destroy(new StackMobNoopCallback());
    }

    public void destroy(StackMobCallback callback) {
        StackMob.getStackMob().delete(getSchemaName(), id, callback);
    }

}