/*
* Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
*
* 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.scvngr.levelup.core.model.factory.json;
import android.support.annotation.NonNull;
import com.scvngr.levelup.core.annotation.LevelUpApi;
import com.scvngr.levelup.core.annotation.LevelUpApi.Contract;
import com.scvngr.levelup.core.util.NullUtils;
import com.scvngr.levelup.core.util.PreconditionUtil;
import net.jcip.annotations.Immutable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* Superclass for JSON parsers that create model objects using a factory design pattern. Any model
* object that wishes to be parsed from JSON should have an implementation of this class.
*
* @param <T> The type of model this factory produces.
*/
@Immutable
@LevelUpApi(contract = Contract.INTERNAL)
public abstract class AbstractJsonModelFactory<T> {
/**
* The type key under which the JSON object can be nested under.
*/
@NonNull
private final String mTypeKey;
/**
* Constructs a new factory.
*
* @param typeKey the key which the object to parse can be nested under. It will usually be the
* name of the object's type:
*
* <pre>
* { 'typeKey' : { 'field1': 'test' } }.
* </pre>
*
* When requesting a single object or a list of objects, the object will be nested under
* this type key. When you are parsing an object as a field of another object, it will
* not be nested under a type key.
*/
public AbstractJsonModelFactory(@NonNull final String typeKey) {
PreconditionUtil.assertNotNull(typeKey, "typeKey");
mTypeKey = typeKey;
}
/**
* @return the type key which this object can be nested under.
*/
@NonNull
protected final String getTypeKey() {
return mTypeKey;
}
/**
* Parse an instance of the model from a {@link JSONObject}.
*
* @param json the {@link JSONObject} to parse from.
* @return an instance of {@code T} parsed from {@code json}.
* @throws JSONException If the model fails to parse.
*/
@NonNull
protected abstract T createFrom(@NonNull final JSONObject json) throws JSONException;
/**
* Parse a list of models from the JSON array passed.
*
* @param jsonArray the jsonArray to parse from.
* @return a {@link List} of model instances.
* @throws JSONException if any of the models fail to parse.
*/
@NonNull
public final List<T> fromList(@NonNull final JSONArray jsonArray) throws JSONException {
PreconditionUtil.assertNotNull(jsonArray, "jsonArray");
final int count = jsonArray.length();
final List<T> objectList = new ArrayList<T>(count);
for (int i = 0; i < count; i++) {
final JSONObject jsonObject = NullUtils.nonNullContract(jsonArray.getJSONObject(i));
objectList.add(from(jsonObject));
}
return objectList;
}
/**
* Parse a model from the JSON object passed, un-nesting from a root element if necessary.
*
* @param jsonObject the JSON representation of the model to parse.
* @return a model instance.
* @throws JSONException if the model fails to parse.
*/
@NonNull
public final T from(@NonNull final JSONObject jsonObject) throws JSONException {
PreconditionUtil.assertNotNull(jsonObject, "jsonObject");
JSONObject objectToParse = jsonObject;
if (jsonObject.has(mTypeKey) && 1 == jsonObject.length()) {
/*
* Root element (probably) found, although we need to check to be sure it's not just a
* sub-element with the same name
*/
try {
objectToParse = NullUtils.nonNullContract(jsonObject.getJSONObject(mTypeKey));
} catch (final JSONException exception) {
/*
* A JSONException can be thrown if the key we are searching for happens to not be
* the key which everything is nested under, but instead the name of a field that
* will not return a JSONObject. IE: errors can be returned nested like:
*
* { 'error' : {'error' : 'the error'} }
*
* or:
*
* { 'error' : 'the error' }
*
* The latter example would thrown an exception when trying to return a JSONObject.
*/
objectToParse = jsonObject; // Just use the object passed; don't un-nest.
}
}
return createFrom(objectToParse);
}
}