/* * BaseModeleDepot.java * CodeNameHippie * * Copyright (c) 2016. Philippe Lafontaine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.pam.codenamehippie.modele.depot; import android.annotation.SuppressLint; import android.content.Context; import android.database.DataSetObservable; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.pam.codenamehippie.HippieApplication; import com.pam.codenamehippie.http.Authentificateur; import com.pam.codenamehippie.http.exception.HttpReponseException; import com.pam.codenamehippie.modele.BaseModele; import com.pam.codenamehippie.modele.OrganismeModele; import com.pam.codenamehippie.modele.UtilisateurModele; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import okhttp3.Call; import okhttp3.Callback; import okhttp3.FormBody; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; /** * Classe patron représentant un dépôt d'objet de type {@link BaseModele}. * <p> * Cette classe est définie comme abstraite pour 2 raisons: * </p> * <ol> * <li> * Elle à été conçue avec l'Intention d'être la classe mère de tous les autres dépôt. * </li> * <li> * En Java, les méthodes dans les interfaces ne peuvent contenir un corps et nous désirons * avoir éviter de la duplication de code inutile. Autrement dit, cette classe tente de * fournir des une implémentation par défaut quand c'est possible. * </li> * </ol> * <p> * L'initialisation d'un dépôt requiert une inspection de sa hiearchie de classe en utilisant * le mécanisme de réflection de Java. Ceci est une opération relativement dispendieuse, par * conséquent nous recommandons de limiter le nombre d'allocation d'instances d'objet de type * dépôt. * </p> * * @param <T> * Type de modèle que le dépot contient. */ public abstract class BaseModeleDepot<T extends BaseModele<T>> { /** * Instance globale de la classe servant à la conversion des objets du dépôt en format JSON. * Ce membre est publique afin de réduire le nombre d'allocation. */ protected final static Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .serializeNulls() .create(); private static final String TAG = BaseModeleDepot.class.getSimpleName(); /** * Contenant qui renferme les objets entretenus par le dépôt. */ protected final ArrayList<T> modeles = new ArrayList<>(); /** * Client http. */ protected final OkHttpClient httpClient; /** * Context pour accèder au ressources string. */ protected final Context context; /** * Verrou de synchronisation. */ protected final Object lock = new Object(); /** * Main thread handler */ protected final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); /** * La valeur du paramètre de type T. */ protected Class classeDeT; /** * Url du des objets du dépôt. */ protected HttpUrl url = HippieApplication.BASE_URL; /** * Url de la dernière requête de peuplement effectuée */ protected HttpUrl urlDeRepeuplement = null; /** * Url pour modifications des objets du dépot. */ protected HttpUrl modifierUrl = null; /** * Url pour les ajouts des objets du dépot */ protected HttpUrl ajoutUrl = null; /** * Url pour les suppressions des objets du dépot */ protected HttpUrl supprimerUrl = null; /** * Liste contenant les objets qui observe le dépôt. */ protected volatile ArrayList<ObservateurDeDepot<T>> observateurs = new ArrayList<>(); /** * Foncteur pour les listes résultantes des requêtes */ protected FiltreDeListe<T> filtreDeListe = null; /** * Initialise les variables commune à tous les dépôts. * * @param context * le context pour aller chercher des ressources string. * @param httpClient * le client http pour utiliser par les dépots pour faire des requêtes au * serveur */ protected BaseModeleDepot(Context context, OkHttpClient httpClient) { Class clazz = this.getClass(); ParameterizedType genericType; // Recherche la première classe générique dans l'heritage. do { genericType = ((ParameterizedType) clazz.getGenericSuperclass()); clazz = super.getClass(); } while (genericType == null); // Recherche le premier paramètre de type qui hérite de BaseModele. for (Type type : genericType.getActualTypeArguments()) { if (BaseModele.class.isAssignableFrom((Class) type)) { synchronized (this.lock) { this.classeDeT = (Class) type; break; } } } this.context = context; this.httpClient = httpClient; } /** * Accesseur de l'url des objet du dépôt * * @return Url du des objets du dépôt. */ public HttpUrl getUrl() { return this.url; } /** * Accesseur du contenu du dépôt. Le contenu du dépôt est toujours le résultat de de la * dernière requête de peuplement. * * @return Le contenu du dépôt */ public ArrayList<T> getModeles() { synchronized (this.lock) { return this.modeles; } } public FiltreDeListe<T> getFiltreDeListe() { synchronized (this.lock) { return this.filtreDeListe; } } /** * Assigne un filtre pour toutes les nouvelles requêtes de peuplement du dépôt. Mettre à null * pour supprimer le filtre * * @param filtreDeListe * Le filtre à mettre pour la requête. */ public void setFiltreDeListe(@Nullable FiltreDeListe<T> filtreDeListe) { synchronized (this.lock) { this.filtreDeListe = filtreDeListe; } } /** * Méthode de sérialisation du modèle en JSON. * * @return le modèle en format JSON. */ public String toJson(T modele) { synchronized (this.lock) { return gson.toJson(modele, this.classeDeT); } } /** * Méthode de désérialisation du modèle en JSON * * @param json * un string formatté en JSON. représentant le modèle * * @return une instance du modèle. * * @throws JsonSyntaxException * Si le json n'est pas convertible en modèle */ @SuppressWarnings("unchecked") public T fromJson(String json) throws JsonSyntaxException { synchronized (this.lock) { return (T) gson.fromJson(json, this.classeDeT); } } /** * Méthode de désérialisation du modèle en JSON * * @param reader * un reader de string formatté en JSON. représentant le modèle * * @return une instance du modèle ou null si le reader ne contient plus rien. * * @throws JsonIOException * S'il y a eu un problème de lecture de json avec le reader * @throws JsonSyntaxException * Si le reader rencontre du JSON malformé. */ @Nullable public T fromJson(JsonReader reader) throws JsonIOException, JsonSyntaxException { synchronized (this.lock) { T result = null; try { JsonToken token = reader.peek(); if (token.equals(JsonToken.BEGIN_ARRAY)) { reader.beginArray(); token = reader.peek(); } switch (token) { case BEGIN_OBJECT: result = gson.fromJson(reader, this.classeDeT); break; case END_ARRAY: reader.endArray(); break; case END_DOCUMENT: reader.close(); } token = reader.peek(); if (token.equals(JsonToken.END_DOCUMENT)) { reader.close(); } } catch (IllegalStateException e) { // Le reader est fermé on retourne le résultat. return result; } catch (IOException e) { this.surErreur(e); } return result; } } /** * Méthode de désérialisation du modèle en JSON * * @param reader * un reader de string formatté en JSON. représentant le modèle * * @return une instance du modèle. * * @throws JsonIOException * S'il y a eu un problème de lecture de json avec le reader * @throws JsonSyntaxException * Si le reader rencontre du JSON malformé. */ public T fromJson(Reader reader) throws IOException, JsonIOException, JsonSyntaxException { return this.fromJson(new JsonReader(reader)); } /** * Méthode pour transformer un objet modèle en {@link okhttp3.FormBody.Builder}. * <p/> * Cette méthode est du code expérimental/prototype. * L'idée ici c'est d'utiliser la réflection java pour créer une form http. * Il serait plus facile de soumettre du json, mais en ce moment, le serveur ne le * prend pas en ce moment… * </p> * * @param modele * le modele à transformer. * * @return un nouveau {@link okhttp3.FormBody.Builder} rempli avec les champs. */ public FormBody.Builder toFormBodyBuilder(@NonNull T modele) { Class clazz = modele.getClass(); FormBody.Builder formBuilder = new FormBody.Builder(); do { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { SerializedName fieldAnnotation = field.getAnnotation(SerializedName.class); boolean old = field.isAccessible(); field.setAccessible(true); if (fieldAnnotation != null) { String serializeName = fieldAnnotation.value(); try { Object value = field.get(modele); // On saute par dessus pour les champs qui sont des modeles pour le moment. if ((value != null) && !(value instanceof BaseModele)) { if (value instanceof Date) { @SuppressLint("SimpleDateFormat") String dateString = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(value); formBuilder.add(serializeName, dateString); } else { formBuilder.add(serializeName, value.toString()); } } } catch (IllegalAccessException e) { e.printStackTrace(); } } field.setAccessible(old); } clazz = clazz.getSuperclass(); } while (clazz != null); return formBuilder; } /** * Méthode pour transformer un objet modèle en {@link okhttp3.FormBody}. * <p/> * Cette méthode est du code expérimental/prototype. * L'idée ici c'est d'utiliser la réflection java pour créer une form http. * Il serait plus facile de soumettre du json, mais en ce moment, le serveur ne le * prend pas en ce moment… * </p> * * @param modele * le modele à transformer en FormBody. * * @return un nouveau {@link okhttp3.FormBody} rempli avec les champs. */ public FormBody toFormBody(@NonNull T modele) { return this.toFormBodyBuilder(modele).build(); } /** * Permet de peupler le dépot. * <p/> * Cette methode est asynchrone et retourne immédiatement. * </p * * @param url * url de la requête. */ protected void peuplerLeDepot(@NonNull HttpUrl url) { synchronized (this.lock) { if ((this.urlDeRepeuplement == null) || (!this.urlDeRepeuplement.equals(url))) { this.urlDeRepeuplement = url; } } Request request = new Request.Builder().url(url).get().build(); // FIXME: surDebutDeRequête devrait être caller quand le dispatcher traite la requête. // Il faudrait soumettre manuellement les calls aux dispatcher… Ça demanderait quand // même assez de travail… Pour les besoins de la cause on va tenter de pas soumettre // plusieurs requêtes en même temps au même dépot. this.surDebutDeRequete(); this.httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "Request failed: " + call.request().toString(), e); BaseModeleDepot.this.surErreur(e); BaseModeleDepot.this.surFinDeRequete(); } @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { Log.e(TAG, "Request failed: " + response.toString()); BaseModeleDepot.this.surErreur(new HttpReponseException(response)); } else { synchronized (BaseModeleDepot.this.lock) { // On vide le dépôt pour faire place au nouveau stock. BaseModeleDepot.this.modeles.clear(); // Le serveur retourne un array. Donc pour supporter un énorme array on // utilise des streams. JsonReader reader = new JsonReader(response.body().charStream()); T modele = BaseModeleDepot.this.fromJson(reader); while (modele != null) { if (BaseModeleDepot.this.filtreDeListe != null) { if (BaseModeleDepot.this.filtreDeListe.appliquer(modele)) { BaseModeleDepot.this.modeles.add(modele); } } else { BaseModeleDepot.this.modeles.add(modele); } modele = BaseModeleDepot.this.fromJson(reader); } } BaseModeleDepot.this.surChangementDeDonnees(); } BaseModeleDepot.this.surFinDeRequete(); } }); } /** * Repeuple le dépôt avec la dernière url utilisée par le dépôt. */ public void repeuplerLedepot() { synchronized (this.lock) { if (this.urlDeRepeuplement != null) { this.peuplerLeDepot(this.urlDeRepeuplement); } } } /** * Méthode qui recherche un modèle selon l'id de l'objet reçu en paramètre. * <p> * Cette methode est asynchrone et retourne immédiatement. * </p> * * @param id * de l'objet */ public void rechercherParId(@NonNull Integer id) { HttpUrl url = this.url.newBuilder().addPathSegment(id.toString()).build(); this.peuplerLeDepot(url); } /** * Ajouter un nouveau modèle dans le système. * * @param modele * le modele à ajout. * @param action * Callback en cas de succes * * @throws UnsupportedOperationException * Si le dépot ne supporte pas l'ajout */ public void ajouterModele(@NonNull T modele, @Nullable final Runnable action) throws UnsupportedOperationException { if (this.ajoutUrl == null) { throw new UnsupportedOperationException("Ce dépôt ne supporte pas l'ajout"); } FormBody.Builder body = this.toFormBodyBuilder(modele); UtilisateurModele uc = ((Authentificateur) this.httpClient.authenticator()).getUtilisateur(); OrganismeModele org = (uc != null) ? uc.getOrganisme() : null; if (org != null) { body.add("donneur_id", org.getId().toString()); } Request request = new Request.Builder().url(this.ajoutUrl).post(body.build()).build(); this.surDebutDeRequete(); this.httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "Request failed: " + call.request().toString(), e); BaseModeleDepot.this.surErreur(e); BaseModeleDepot.this.surFinDeRequete(); } @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { Log.e(TAG, "Request failed: " + response.toString()); BaseModeleDepot.this.surErreur(new HttpReponseException(response)); } else { if (action != null) { BaseModeleDepot.this.runOnUiThread(action); } } BaseModeleDepot.this.surFinDeRequete(); } }); } /** * Modifie un é présent dans le dépôt correspondant selon l'id de l'objet reçu en * paramètre. * * @param modele * Le modèle à modifie * @param action * Callback en cas de succes * * @throws UnsupportedOperationException * Si le dépot ne supporte pas l'ajout */ public void modifierModele(@NonNull T modele, @Nullable final Runnable action) throws UnsupportedOperationException { if (this.modifierUrl == null) { throw new UnsupportedOperationException("Ce dépôt ne supporte pas la modification"); } FormBody.Builder body = this.toFormBodyBuilder(modele); UtilisateurModele uc = ((Authentificateur) this.httpClient.authenticator()).getUtilisateur(); OrganismeModele org = (uc != null) ? uc.getOrganisme() : null; if (org != null) { body.add("donneur_id", org.getId().toString()); } Request request = new Request.Builder().url(this.modifierUrl).post(body.build()).build(); this.surDebutDeRequete(); this.httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "Request failed: " + call.request().toString(), e); BaseModeleDepot.this.surErreur(e); BaseModeleDepot.this.surFinDeRequete(); } @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { Log.e(TAG, "Request failed: " + response.toString()); BaseModeleDepot.this.surErreur(new HttpReponseException(response)); } else { if (action != null) { BaseModeleDepot.this.runOnUiThread(action); } } BaseModeleDepot.this.surFinDeRequete(); } }); } /** * Envoi une commande de suppression de données au serveur. * <p> * Cette méthode est asynchrone et retourne immédiatement.<br/> * Cette méthode est équivalente à {@code supprimerModele(modele, null)}. * </p> * * @param modele * l'objet à supprimer. * * @see BaseModeleDepot#supprimerModele(BaseModele, Runnable) */ public void supprimerModele(T modele) { this.supprimerModele(modele, null); } /** * Envoi une commande de suppression de données au serveur. * <p> * Cette méthode est asynchrone et retourne immédiatement. * </p> * * @param modele * l'objet à supprimer * @param action * une action à executer en cas de succès. Cette action est exécutée sur le main * thread. */ public void supprimerModele(T modele, @Nullable final Runnable action) { if (this.supprimerUrl == null) { throw new UnsupportedOperationException("Ce dépot ne supporte pas la suppression"); } HttpUrl url = this.supprimerUrl.newBuilder() .addPathSegment(modele.getId().toString()) .build(); Request request = new Request.Builder().url(url).get().build(); this.httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { BaseModeleDepot.this.surErreur(e); } @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { HttpReponseException e = new HttpReponseException(response); BaseModeleDepot.this.surErreur(e); } else { BaseModeleDepot.this.repeuplerLedepot(); if (action != null) { BaseModeleDepot.this.runOnUiThread(action); } } } }); } /** * Ajoute un objet implémentant l'interface {@link ObservateurDeDepot} dans la listes des * observateurs du dépôt. L'objet ajouté reçoit des notifications du dépôt à l'aide des * méthodes de l'interface {@link ObservateurDeDepot}. * * @param observateur * L'objet qui va recevoir les callbacks. * * @see {@link android.database.DataSetObservable#registerObserver(Object)} */ public void ajouterUnObservateur(@NonNull ObservateurDeDepot<T> observateur) { synchronized (this.lock) { if (this.observateurs.contains(observateur)) { throw new IllegalStateException("L'observateur " + observateur + "est déjà ajouté"); } this.observateurs.add(observateur); } } /** * Supprime un objet implémentant l'interface {@link ObservateurDeDepot} de la liste des * observateurs du dépôt. L'objet supprimé cesse de recevoir des notifications du dépôt. * * @param observateur * L'objet à enlever de la liste des observateur. * * @see {@link android.database.DataSetObservable#unregisterObserver(Object)} */ public void supprimerUnObservateur(@NonNull ObservateurDeDepot<T> observateur) { synchronized (this.lock) { int index = this.observateurs.indexOf(observateur); if (index == -1) { throw new IllegalStateException("L'observateur " + observateur + "n'est pas déjà " + "ajouté"); } this.observateurs.remove(index); } } /** * Vide la liste des observateurs. * * @see {@link DataSetObservable#unregisterAll()} */ public void supprimerTousLesObservateurs() { synchronized (this.lock) { this.observateurs.clear(); } } /** * Notifie tous les observateurs du dépôt qu'une requête a démarré. * * @see {@link ObservateurDeDepot#surDebutDeRequete()} */ public void surDebutDeRequete() { Runnable action = new Runnable() { public void run() { synchronized (BaseModeleDepot.this.lock) { if (!BaseModeleDepot.this.observateurs.isEmpty()) { for (ObservateurDeDepot<T> obs : BaseModeleDepot.this.observateurs) { obs.surDebutDeRequete(); } } } } }; this.runOnUiThread(action); } /** * Notifie tous les observateurs du dépôt qu'il y a eu un changement dans les données du dépôt. */ public void surChangementDeDonnees() { Runnable action = new Runnable() { public void run() { synchronized (BaseModeleDepot.this.lock) { if (!BaseModeleDepot.this.observateurs.isEmpty()) { List<T> resultat = Collections.unmodifiableList(BaseModeleDepot.this .modeles); for (ObservateurDeDepot<T> obs : BaseModeleDepot.this.observateurs) { obs.surChangementDeDonnees(resultat); } } } } }; this.runOnUiThread(action); } /** * Notifie tous les observateurs du dépôt qu'il y a eu une erreur lors d'une requête. */ public void surErreur(final IOException e) { Runnable action = new Runnable() { public void run() { synchronized (BaseModeleDepot.this.lock) { if (!BaseModeleDepot.this.observateurs.isEmpty()) { for (ObservateurDeDepot<T> obs : BaseModeleDepot.this.observateurs) { obs.surErreur(e); } } } } }; this.runOnUiThread(action); } /** * Notifie tous les observateurs du dépôt qu'une requête a terminée. * * @see {@link ObservateurDeDepot#surDebutDeRequete()} */ public void surFinDeRequete() { Runnable action = new Runnable() { public void run() { synchronized (BaseModeleDepot.this.lock) { if (!BaseModeleDepot.this.observateurs.isEmpty()) { for (ObservateurDeDepot<T> obs : BaseModeleDepot.this.observateurs) { obs.surFinDeRequete(); } } } } }; this.runOnUiThread(action); } /** * Réimplémentation de {@link android.app.Activity#runOnUiThread(Runnable)} * * @param action * truc à rouler sur le main thread * * @see android.app.Activity#runOnUiThread(Runnable) */ protected void runOnUiThread(Runnable action) { if (Thread.currentThread() != this.mainThreadHandler.getLooper().getThread()) { this.mainThreadHandler.post(action); } else { action.run(); } } }