diff --git a/APIKeys b/APIKeys new file mode 100644 index 0000000..7e0b084 --- /dev/null +++ b/APIKeys @@ -0,0 +1 @@ +newsapi 0e72f765c5c84313ae31a5a7e9e61735 \ No newline at end of file diff --git a/Classes/ApiCall.java b/Classes/ApiCall.java index 895318f..c78cad2 100644 --- a/Classes/ApiCall.java +++ b/Classes/ApiCall.java @@ -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() { + //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; + } + } + + //si il y a eu une erreur durant le delayedallback, on stope le thread + if(this.shouldStop) { + return; + } try { - //Send request - DataOutputStream wr = new DataOutputStream (connection.getOutputStream ()); - wr.flush (); - wr.close (); + //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 (); + } - //Get Response + //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> 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(); + } + } + } } diff --git a/Classes/Category.java b/Classes/Category.java new file mode 100644 index 0000000..d5a2cf8 --- /dev/null +++ b/Classes/Category.java @@ -0,0 +1,5 @@ +package Classes; + +public enum Category { + business, entertainment, gaming, general, healthAndMedical, music, politics, scienceAndNature, sport, technology, all +} diff --git a/Classes/Languages.java b/Classes/Languages.java new file mode 100644 index 0000000..2a47012 --- /dev/null +++ b/Classes/Languages.java @@ -0,0 +1,5 @@ +package Classes; + +public enum Languages { + fr,en +} diff --git a/Classes/SortTypes.java b/Classes/SortTypes.java new file mode 100644 index 0000000..a7057de --- /dev/null +++ b/Classes/SortTypes.java @@ -0,0 +1,5 @@ +package Classes; + +public enum SortTypes { + relevancy, popularity, publishedAt +} diff --git a/Interfaces/Callback.java b/Interfaces/Callback.java index 09de622..6689331 100644 --- a/Interfaces/Callback.java +++ b/Interfaces/Callback.java @@ -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(); } diff --git a/Interfaces/DelayedCallback.java b/Interfaces/DelayedCallback.java new file mode 100644 index 0000000..8e03db9 --- /dev/null +++ b/Interfaces/DelayedCallback.java @@ -0,0 +1,11 @@ +package Interfaces; + +import java.util.HashMap; + +import javafx.util.Pair; + +public interface DelayedCallback { + + public Pair> call(); + +} diff --git a/Interfaces/Observable.java b/Interfaces/Observable.java new file mode 100644 index 0000000..a24d9de --- /dev/null +++ b/Interfaces/Observable.java @@ -0,0 +1,15 @@ +/** + * + */ +package Interfaces; + +/** + * @author lucas + * + */ +public interface Observable { + + public void addObserver(String key , EventObserver o); + public void removeObserver(String key); + +} diff --git a/controller/HeaderMenu.java b/controller/HeaderMenu.java index 8eeb1bd..1565e01 100644 --- a/controller/HeaderMenu.java +++ b/controller/HeaderMenu.java @@ -35,7 +35,7 @@ public class HeaderMenu{ menuItem.setOnMousePressed(new EventHandler() { @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")); } }); diff --git a/controller/RootLayout.java b/controller/RootLayout.java index f508bb1..95cfe0b 100644 --- a/controller/RootLayout.java +++ b/controller/RootLayout.java @@ -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 headers = new HashMap(); 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"); } } diff --git a/model/LangModel.java b/model/LangModel.java new file mode 100644 index 0000000..ca7ba98 --- /dev/null +++ b/model/LangModel.java @@ -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; + } + +} diff --git a/model/NewsListModel.java b/model/NewsListModel.java new file mode 100644 index 0000000..8fccc2e --- /dev/null +++ b/model/NewsListModel.java @@ -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 sources; + //liste des observers + private HashMap observers; + //liste des news finale + private ArrayList 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(20); + this.observers = new HashMap(); + this.news = new ArrayList(); + } + + 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(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(); + JSONArray arr = response.getJSONArray("articles"); + for(int i = 0;i> 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>("",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>(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 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); + + } + + +} diff --git a/model/NewsModel.java b/model/NewsModel.java new file mode 100644 index 0000000..ae8d9f5 --- /dev/null +++ b/model/NewsModel.java @@ -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; + } + +}