Implémentation de la classe de gestion des API + implémentation du NewsListModel

This commit is contained in:
SeekDaSky 2017-11-15 22:53:52 +01:00
parent 24fb79745d
commit bc62a3b65c
13 changed files with 570 additions and 27 deletions

1
APIKeys Normal file
View File

@ -0,0 +1 @@
newsapi 0e72f765c5c84313ae31a5a7e9e61735

View File

@ -11,25 +11,52 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
import Interfaces.Callback;
import Interfaces.DelayedCallback;
import Interfaces.Event;
import Interfaces.EventObserver;
import javafx.util.Pair;
public class ApiCall implements Runnable {
public class ApiCall implements Runnable, EventObserver {
//Methode appelée une fois que la requete s'est terminé
private Callback callback;
//connection HTTP
private HttpURLConnection connection;
//méthode HTTP
private String method;
//est ce que cet appel d'API attend un évenement pour se lancer
private Boolean isDelayed = false;
//Methode appellée une fois l'évenement arrivé, il retourne la nouvelle URL et les nouveaux parametres POST
private DelayedCallback delayedCallback;
//ID et Type de l'évenement attendu
private String eventID;
private String eventType;
//objet contenant le thread de l'appel
private Thread thread;
//si une erreur surviens durant le delayedCallback, la boucle run doit tout arreter, cette variable sert a notifier run() de ça
private Boolean shouldStop = false;
public ApiCall(String URL, String method, Callback call) {
this.callback = call;
this.method = method;
try {
//création de la connection
URL url = new URL(URL);
this.connection = (HttpURLConnection) url.openConnection();
this.connection.setRequestMethod(method);
this.connection.setDoInput(true);
this.connection.setDoOutput(true);
//si on fait une requete GET, il faut interdire l'envoie de donnée POST
if(method == "GET") {
this.connection.setDoOutput(false);
}else {
this.connection.setDoOutput(true);
}
} catch (IOException e) {
System.out.println("Impossible d'ouvrir l'URL: "+URL);
this.callback.onError();
}
}
@ -38,19 +65,39 @@ public class ApiCall implements Runnable {
while (it.hasNext()) {
Map.Entry pair = (Map.Entry)it.next();
this.connection.setRequestProperty((String)pair.getKey(), (String)pair.getValue());
it.remove(); // avoids a ConcurrentModificationException
it.remove(); // évite les erreurs en cas de modification concurente
}
}
@Override
public void run() {
try {
//Send request
DataOutputStream wr = new DataOutputStream (connection.getOutputStream ());
wr.flush ();
wr.close ();
//si notre appel est delayed, on endort le thread
if(this.isDelayed) {
try {
//on doit se synchroniser avec le thread avant de l'endormir
synchronized(this.thread) {
this.thread.wait();
}
} catch (InterruptedException e) {
this.callback.onError();
return;
}
}
//Get Response
//si il y a eu une erreur durant le delayedallback, on stope le thread
if(this.shouldStop) {
return;
}
try {
//On envoie les paramètres POST si besoin
if(this.method != "GET") {
DataOutputStream wr = new DataOutputStream (connection.getOutputStream ());
//TODO: implémenter la gestion des parametres POST
wr.flush ();
wr.close ();
}
//on récupère la réponse de la requete
InputStream is = connection.getInputStream();
BufferedReader rd = new BufferedReader(new InputStreamReader(is));
String line;
@ -60,16 +107,69 @@ public class ApiCall implements Runnable {
}
rd.close();
this.callback.call(response.toString());
//transformation en objet JSON
try {
//raplce null values by empty strings
JSONObject json = new JSONObject(response.toString().replaceAll(":null,", ":\"\","));
this.callback.onSuccess(json);
}catch(JSONException e) {
this.callback.onError();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
this.callback.onError();
}
}
public void send() {
new Thread(this).start();
//send sert juste a lancer la boucle principale run()
this.thread = new Thread(this);
this.thread.start();
}
public void setAsDelayedCall(String eventID, String eventType, DelayedCallback call) {
//on enregistre tout ce dont on a besoin pour traiter l'évenement
this.isDelayed = true;
this.delayedCallback = call;
this.eventID = eventID;
this.eventType = eventType;
}
@Override
public void handleEvent(Event e) {
//si on est un appel delayed et que l'évenement est le bon, on lance le delayedCallback
if(this.isDelayed && e.getObjectId() == this.eventID && e.getEventType() == this.eventType) {
Pair<String,HashMap<String,String>> returned = this.delayedCallback.call();
if(this.method == "GET") {
try {
//on met a jour la connection HTTP avec la nouvelle URL en gardant l'ancienne méthode HTTP
HttpURLConnection newConnection = (HttpURLConnection) new URL(returned.getKey()).openConnection();
newConnection.setRequestMethod(this.connection.getRequestMethod());
newConnection.setDoOutput(this.connection.getDoOutput());
this.connection = newConnection;
//on a fini de mettre a jour la connection, le thread principal peut reprendre
synchronized(this.thread) {
this.thread.notify();
}
} catch (IOException e1) {
//une erreur est survenue, on stope tout
this.callback.onError();
this.shouldStop = true;
synchronized(this.thread) {
this.thread.notify();
}
}
}else {
//TODO: implémenter les param POST
}
}else {
this.callback.onError();
this.shouldStop = true;
synchronized(this.thread) {
this.thread.notify();
}
}
}
}

5
Classes/Category.java Normal file
View File

@ -0,0 +1,5 @@
package Classes;
public enum Category {
business, entertainment, gaming, general, healthAndMedical, music, politics, scienceAndNature, sport, technology, all
}

5
Classes/Languages.java Normal file
View File

@ -0,0 +1,5 @@
package Classes;
public enum Languages {
fr,en
}

5
Classes/SortTypes.java Normal file
View File

@ -0,0 +1,5 @@
package Classes;
public enum SortTypes {
relevancy, popularity, publishedAt
}

View File

@ -1,7 +1,11 @@
package Interfaces;
import org.json.JSONObject;
public interface Callback {
public void call(String response);
public void onSuccess(JSONObject response);
public void onError();
}

View File

@ -0,0 +1,11 @@
package Interfaces;
import java.util.HashMap;
import javafx.util.Pair;
public interface DelayedCallback {
public Pair<String,HashMap<String,String>> call();
}

View File

@ -0,0 +1,15 @@
/**
*
*/
package Interfaces;
/**
* @author lucas
*
*/
public interface Observable {
public void addObserver(String key , EventObserver o);
public void removeObserver(String key);
}

View File

@ -35,7 +35,7 @@ public class HeaderMenu{
menuItem.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
HeaderMenu.this.observer.handleEvent(new Classes.Event(menuItem.getId(),"click"));
HeaderMenu.this.observer.handleEvent(new Classes.Event(menuItem.getId(),"changeMainLayout"));
}
});

View File

@ -6,6 +6,9 @@ import java.util.HashMap;
import org.json.JSONObject;
import Classes.ApiCall;
import Classes.Category;
import Classes.Languages;
import Classes.SortTypes;
import Classes.css.user.ContextBuilder;
import Classes.css.user.Header;
import Classes.css.user.MenuContainer;
@ -15,9 +18,12 @@ import Interfaces.EventObserver;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.MenuBar;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
import model.LangModel;
import model.NewsListModel;
public class RootLayout extends Application implements EventObserver {
@ -64,9 +70,6 @@ public class RootLayout extends Application implements EventObserver {
}
public void loadRootLayout(){
try{
@ -97,28 +100,53 @@ public class RootLayout extends Application implements EventObserver {
launch(args);
}
@Override
public void handleEvent(Event e) {
/*
HashMap<String,String> headers = new HashMap<String,String>();
headers.put("Referer", "http://www.wordreference.com");
ApiCall call = new ApiCall("http://api.wordreference.com/1/json/enfr/grin","GET",new Callback() {
@Override
public void call(String response) {
JSONObject json = new JSONObject(response);
System.out.println(json.toString());
public void onSuccess(JSONObject response) {
System.out.println(response.toString());
}
@Override
public void onError() {
System.out.println("APICall error");
}
});
call.addHeaders(headers);
call.send();
call.send();*/
System.out.println("Object "+e.getObjectId()+" fired a "+e.getEventType()+" event");
switch(e.getEventType()){
case "changeMainLayout":
this.handleMainLayoutChange(e.getObjectId());
break;
case "NewsQuerySuccess":
System.out.println(NewsListModel.getInstance().getNews().size()+" News ont été trouvé");
if(NewsListModel.getInstance().getNews().size() != 0) {
System.out.println("La description du premier article est: "+NewsListModel.getInstance().getNews().get(0).getDescription());
}
break;
case "NewsQueryFailed":
System.out.println("une erreur est survenue");
break;
}
}
public void handleMainLayoutChange(String layout) {
NewsListModel.getInstance().addObserver("MainClass", this);
NewsListModel.getInstance().setCategory(Category.business);
NewsListModel.getInstance().setSortType(SortTypes.publishedAt);
NewsListModel.getInstance().query("bitcoin");
}
}

40
model/LangModel.java Normal file
View File

@ -0,0 +1,40 @@
package model;
import Classes.Languages;
public class LangModel {
private static LangModel instance;
private Languages fromLang = Languages.fr;
private Languages toLang = Languages.en;
private LangModel() {
}
public static LangModel getInstance() {
if(LangModel.instance == null) {
LangModel.instance = new LangModel();
}
return LangModel.instance;
}
public void setFromLang(Languages lang) {
this.fromLang = lang;
}
public Languages getFromLang() {
return this.fromLang;
}
public void setToLang(Languages lang) {
this.toLang = lang;
}
public Languages getToLang() {
return this.toLang;
}
}

252
model/NewsListModel.java Normal file
View File

@ -0,0 +1,252 @@
package model;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import Classes.ApiCall;
import Classes.Category;
import Classes.Event;
import Classes.SortTypes;
import Interfaces.Callback;
import Interfaces.DelayedCallback;
import Interfaces.EventObserver;
import Interfaces.Observable;
import javafx.util.Pair;
public class NewsListModel implements Observable{
//instance du singleton
private static NewsListModel instance;
//Clé d'API
private String APIKey;
//catégorie choise parl'utilisateur
private Category cat = Category.all;
//est ce que le modele est en train de récupérer les sources
private Boolean isRetreivingSources = false;
//est ce que l'api a rencontré une erreur
private Boolean apiError = false;
//liste des sources
private ArrayList<String> sources;
//liste des observers
private HashMap<String,EventObserver> observers;
//liste des news finale
private ArrayList<NewsModel> news;
//critère de tri
private SortTypes sortType = SortTypes.relevancy;
//critère de recherche de l'utilisateur
private String query = "";
private NewsListModel(String APIKey) {
this.APIKey = APIKey;
//comme la liste des sources retournée par l'API est de 20 éléments max, pas la peine de déclarer une arraylist sans borne
this.sources = new ArrayList<String>(20);
this.observers = new HashMap<String,EventObserver>();
this.news = new ArrayList<NewsModel>();
}
public static NewsListModel getInstance() {
if(NewsListModel.instance == null) {
NewsListModel.instance = new NewsListModel("0e72f765c5c84313ae31a5a7e9e61735");
}
return NewsListModel.instance;
}
public void setCategory(Category cat) {
if(cat == Category.all) {
this.sources = null;
this.cat = cat;
return;
}
//on vide la liste des sources
this.sources = new ArrayList<String>(20);
this.cat = cat;
//on créé l'URL d'appel de l'API
String lang = LangModel.getInstance().getToLang().name();
String URL = "http://beta.newsapi.org/v2/sources?language="+lang;
//on rajoute la catégorie
if(cat != Category.all) {
URL += "&category="+cat.name();
}
//on rajoute la clé d'api
URL += "&apiKey="+this.APIKey;
//création de l'appel
ApiCall api = new ApiCall(URL,"GET",new Callback() {
@Override
public void onSuccess(JSONObject response) {
//on récupère toutes les sources (dans la limite de 20 sources)
JSONArray arr = response.getJSONArray("sources");
for(int i = 0;i<arr.length() && i<20;i++) {
NewsListModel.this.sources.add(((JSONObject)arr.get(i)).getString("id"));
}
NewsListModel.this.isRetreivingSources = false;
NewsListModel.this.apiError = false;
//tout s'est bien passé, on peut notifier du succès
NewsListModel.this.notifyObservers(new Event("NewsModel","SourcesUpdated"));
}
@Override
public void onError() {
//on notifie de l'échec et on garde en mémoire le fait que l'on a échoué (afin d'annuler la recherche de news qui va suivre)
NewsListModel.this.isRetreivingSources = false;
NewsListModel.this.apiError = true;
NewsListModel.this.notifyObservers(new Event("NewsModel","SourcesUpdateFailed"));
}
});
this.isRetreivingSources = true;
api.send();
}
public Category getCategory() {
return this.cat;
}
public void query(String q) {
//si la recherche de source s'est fini avec une erreur, pas la peine de chercher des articles
if(this.apiError) {
NewsListModel.this.notifyObservers(new Event("NewsModel","NewsQueryFailed"));
this.apiError = false;
return;
}
//on créer l'URL de l'appel
String lang = LangModel.getInstance().getToLang().name();
String URL = "http://beta.newsapi.org/v2/everything?apiKey="+this.APIKey+"&language="+lang+"&q="+q+"&sortBy="+this.sortType.name();
//on ajoute la liste des sources a l'URL de la requete
if(NewsListModel.this.sources != null && NewsListModel.this.sources.size() != 0) {
String sources = "&sources=";
for(String s : NewsListModel.this.sources) {
sources += s+",";
}
//on enlève la dernière virgule
sources = sources.replaceAll(",$", "");
URL += sources;
}
ApiCall api = new ApiCall(URL,"GET",new Callback() {
@Override
public void onSuccess(JSONObject response) {
//on parcours le JSON en créant les objets NewsModel corespondant
NewsListModel.this.news = new ArrayList<NewsModel>();
JSONArray arr = response.getJSONArray("articles");
for(int i = 0;i<arr.length();i++) {
JSONObject jsonNews = (JSONObject)arr.get(i);
NewsModel news = new NewsModel();
news.setAuthor(jsonNews.getString("author"))
.setDescription(jsonNews.getString("description"))
.setTitle(jsonNews.getString("title"))
.setNewsURL(jsonNews.getString("url"))
.setImageURL(jsonNews.getString("urlToImage"))
.setSource(jsonNews.getJSONObject("source").getString("name"));
try {
news.setDate(jsonNews.getString("publishedAt"));
}catch (ParseException e) {
news.setDate(new Date());
}
NewsListModel.this.news.add(news);
}
NewsListModel.this.query = q;
//ne pas oublier d'enlever l'api des observer, sinon il y a des risques de récurrence
NewsListModel.this.removeObserver("newsApiCall");
NewsListModel.this.notifyObservers(new Event("NewsModel","NewsQuerySuccess"));
}
@Override
public void onError() {
//L'appel a échoué :(
System.out.println("Error");
NewsListModel.this.removeObserver("newsApiCall");
NewsListModel.this.notifyObservers(new Event("NewsModel","NewsQueryFailed"));
}
});
//on delay uniquement si on est en train de récupérer les sources
if(this.isRetreivingSources) {
api.setAsDelayedCall("NewsModel", "SourcesUpdated", new DelayedCallback() {
@Override
public Pair<String, HashMap<String, String>> call() {
//si l'appel d'api précédent a échoué, on fait échouer l'appel actuel avec une URL vide
if(NewsListModel.this.apiError) {
//on reset la variable d'erreur pour que l'utilisateur puisse re-essayer plus tard
NewsListModel.this.apiError = false;
return new Pair<String,HashMap<String,String>>("",null);
}
String lang = LangModel.getInstance().getToLang().name();
String URL = "http://beta.newsapi.org/v2/everything?apiKey="+NewsListModel.this.APIKey+"&language="+lang
+"&q="+q+"&sortBy="+NewsListModel.this.sortType.name();
//on ajoute la liste des sources a l'URL de la requete
if(NewsListModel.this.sources != null && NewsListModel.this.sources.size() != 0) {
String sources = "&sources=";
for(String s : NewsListModel.this.sources) {
sources += s+",";
}
//on enlève la dernière virgule
sources = sources.replaceAll(",$", "");
URL += sources;
}
return new Pair<String,HashMap<String,String>>(URL,null);
}
});
this.addObserver("newsApiCall",api);
}
api.send();
}
public void setSortType(SortTypes t) {
this.sortType = t;
this.query(this.query);
}
public void notifyObservers(Event e) {
for(Object key : this.observers.keySet().toArray()) {
this.observers.get(key).handleEvent(e);
}
}
public ArrayList<NewsModel> getNews(){
return this.news;
}
@Override
public void addObserver(String key, EventObserver o) {
this.observers.put(key, o);
}
@Override
public void removeObserver(String key) {
this.observers.remove(key);
}
}

77
model/NewsModel.java Normal file
View File

@ -0,0 +1,77 @@
package model;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class NewsModel {
private String author;
private String source;
private String imageURL;
private String newsURL;
private String title;
private String description;
private Date date;
public String getAuthor() {
return author;
}
public NewsModel setAuthor(String author) {
this.author = author;
return this;
}
public String getSource() {
return source;
}
public NewsModel setSource(String source) {
this.source = source;
return this;
}
public String getImageURL() {
return imageURL;
}
public NewsModel setImageURL(String imageURL) {
this.imageURL = imageURL;
return this;
}
public String getNewsURL() {
return newsURL;
}
public NewsModel setNewsURL(String newsURL) {
this.newsURL = newsURL;
return this;
}
public String getTitle() {
return title;
}
public NewsModel setTitle(String title) {
this.title = title;
return this;
}
public String getDescription() {
return description;
}
public NewsModel setDescription(String description) {
this.description = description;
return this;
}
public Date getDate() {
return date;
}
public NewsModel setDate(String date) throws ParseException {
//on déclare le format de date
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ");
date = date.replace("Z", "+0000");
this.date = fmt.parse(date);
return this;
}
public NewsModel setDate(Date date){
this.date = date;
return this;
}
}