001/**
002 * Copyright (c) 2012, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.twitter;
031
032import java.io.IOException;
033import java.lang.reflect.Field;
034import java.lang.reflect.Modifier;
035import java.text.ParseException;
036import java.util.ArrayList;
037import java.util.Map;
038import java.util.Scanner;
039
040import org.apache.log4j.Logger;
041import org.joda.time.DateTime;
042import org.joda.time.format.DateTimeFormat;
043import org.joda.time.format.DateTimeFormatter;
044import org.openimaj.io.IOUtils;
045
046import com.google.gson.Gson;
047
048/**
049 * A USMFstatus. A java object representation of the Unified Social Media
050 * Format. This object can be empty constructed to do default reads from USMF
051 * JSON, or be given a GeneralJSON class for a JSON object it should expect to
052 * read from JSON and convert to USMF. Translation from alternative JSON sources
053 * relies on the extension of the GeneralJSON class for that format.
054 *
055 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk), Sina Samangooei
056 *         (ss@ecs.soton.ac.uk), Laurence Willmore (lgw1e10@ecs.soton.ac.uk)
057 *
058 */
059public class USMFStatus extends GeneralJSON implements Cloneable {
060        private static final Logger logger = Logger.getLogger(USMFStatus.class);
061        private transient Class<? extends GeneralJSON> generalJSONclass; // class of
062        // the
063        // source.
064
065        /**
066         * Service Name
067         */
068        public String service;
069        /**
070         * Unique ID
071         */
072        public long id;
073        /**
074         * Latitude/Longitude content creation location
075         */
076        public double[] geo;
077        /**
078         * Application used to create this posting
079         */
080        public String application;
081        /**
082         * Plain Language content creation location
083         */
084        public String location;
085        /**
086         * Date posted
087         */
088        public String date;
089        /**
090         * User friendly link to content
091         */
092        public String source;
093        /**
094         * Microblog text / Video Title / Etc
095         */
096        public String text;
097        /**
098         * Full post text / Decription
099         */
100        public String description;
101        /**
102         * Related Keywords
103         */
104        public ArrayList<String> keywords;
105        /**
106         * Category of content
107         */
108        public String category;
109        /**
110         * Duration of content (if video)
111         */
112        public long duration;
113        /**
114         * Number of users who "liked" this
115         */
116        public int likes;
117        /**
118         * Number of users who "disliked" this
119         */
120        public int dislikes;
121        /**
122         * Number of users who "favorited" this
123         */
124        public int favorites;
125        /**
126         * Number of users who "commented" this
127         */
128        public int comments;
129        /**
130         * Number of users who "rated" this
131         */
132        public int rates;
133        /**
134         * Average "rating" of content
135         */
136        public int rating;
137        /**
138         * Minimum "rating" of content
139         */
140        public int min_rating;
141        /**
142         * Maximum "rating" of content
143         */
144        public int max_rating;
145        /**
146         * User object for User Fields
147         */
148        public User user;
149        /**
150         * List of to users
151         */
152        public ArrayList<User> to_users;
153
154        /**
155         * Reply to
156         */
157        public User reply_to;
158        /**
159         * List of links
160         */
161        public ArrayList<Link> links;
162
163        /**
164         * the ISO A2 country code
165         */
166        public String country_code;
167
168        private boolean invalid = false;
169
170        /**
171         * Constructor used if the input JSON is not a USMF json string.
172         *
173         * @param generalJSONclass
174         *            : The class of the GeneralJSON extension.
175         */
176        public USMFStatus(Class<? extends GeneralJSON> generalJSONclass) {
177                this.generalJSONclass = generalJSONclass;
178                this.to_users = new ArrayList<USMFStatus.User>();
179                this.links = new ArrayList<USMFStatus.Link>();
180                this.user = new User();
181                this.keywords = new ArrayList<String>();
182        }
183
184        /**
185         * Empty constructor for reading from USMF json strings.
186         */
187        public USMFStatus() {
188                this.generalJSONclass = USMFStatus.class;
189                this.to_users = new ArrayList<USMFStatus.User>();
190                this.links = new ArrayList<USMFStatus.Link>();
191                this.user = new User();
192                this.keywords = new ArrayList<String>();
193        }
194
195        /**
196         * @return the type of json that backs this instance (used primarily for
197         *         reading)
198         */
199        public Class<? extends GeneralJSON> getGeneralJSONClass() {
200                return this.generalJSONclass;
201        }
202
203        /**
204         * set the type of json that backs this instance (used primarily for
205         * reading)
206         *
207         * @param g
208         */
209        public void setGeneralJSONClass(Class<? extends GeneralJSON> g) {
210                this.generalJSONclass = g;
211        }
212
213        /**
214         * @return the USMF is either a delete notice, a scrub geo notice or some
215         *         other non-status USMF
216         */
217        public boolean isInvalid() {
218                return invalid;
219        }
220
221        @Override
222        public void readASCII(Scanner in) throws IOException {
223                final String line = (in.nextLine());
224                fillFromString(line);
225        }
226
227        /**
228         * Used by readASCII(), and available for external use to fill this
229         * USMFStatus with the information held in the line
230         *
231         * @param line
232         *            = json string in the format specified by the constructor of
233         *            this USMFStatus (if empty constructor, expects a USMFSStatus
234         *            json string)
235         */
236        public void fillFromString(String line) {
237                GeneralJSON jsonInstance = null;
238                try {
239                        jsonInstance = IOUtils.newInstance(generalJSONclass);
240                        jsonInstance = jsonInstance.instanceFromString(line);
241                } catch (final Throwable e) {
242                        logger.debug("Error parsing USMF: " + e.getMessage());
243                }
244
245                if (jsonInstance == null) {
246                        this.text = line;
247                } else {
248                        jsonInstance.fillUSMF(this);
249                }
250
251                if (this.text == null && this.analysis.size() == 0) {
252                        this.invalid = true;
253                        return;
254                }
255                this.invalid = false;
256        }
257
258        @Override
259        public GeneralJSON instanceFromString(String line) {
260                GeneralJSON jsonInstance = null;
261                try {
262                        jsonInstance = gson.fromJson(line, generalJSONclass);
263                } catch (final Throwable e) {
264                        logger.debug("Error parsing USMF: " + e.getMessage());
265                }
266                return jsonInstance;
267        }
268
269        /*
270         * Helper method that populates this instance of a USMFStatus with the data
271         * from a USMFStatus constructed from json
272         */
273        private void fillFrom(USMFStatus read) {
274                for (final Field field : USMFStatus.class.getFields()) {
275                        if (Modifier.isPublic(field.getModifiers())) {
276                                try {
277                                        field.set(this, field.get(read));
278                                } catch (final IllegalArgumentException e) {
279                                        e.printStackTrace();
280                                } catch (final IllegalAccessException e) {
281                                        e.printStackTrace();
282                                }
283                        }
284                }
285        }
286
287        @Override
288        public String toString() {
289                return this.text;
290        }
291
292        /**
293         * @return convert this {@link USMFStatus} to JSON using {@link Gson}
294         */
295        public String toJson() {
296                return gson.toJson(this, this.getClass());
297        }
298
299        @Override
300        public boolean equals(Object obj) {
301                if (!(obj instanceof USMFStatus))
302                        return false;
303                final USMFStatus status = (USMFStatus) obj;
304                // String statusStr = gson.toJson(status);
305                // String thisStr = gson.toJson(this);
306                boolean equal = true;
307                equal = equalNonAnalysed(status);
308                if (!equal)
309                        return false;
310                equal = equalAnalysed(status);
311                return equal;
312        }
313
314        private boolean equalAnalysed(USMFStatus status) {
315                final Map<String, Object> thatanal = status.analysis;
316                final Map<String, Object> thisanal = this.analysis;
317                for (final String key : thatanal.keySet()) {
318                        // if this contains the same key, and the values for the key are
319                        // equal
320                        if (!thisanal.containsKey(key))
321                                return false;
322                        final Object thisobj = thisanal.get(key);
323                        final Object thatobj = thatanal.get(key);
324                        if (thisobj.equals(thatobj))
325                                continue;
326                        return false;
327                }
328                return true;
329        }
330
331        private boolean equalNonAnalysed(USMFStatus that) {
332                final Field[] fields = this.getClass().getDeclaredFields();
333                for (final Field field : fields) {
334                        if (field.getName() == "analysis"
335                                        || Modifier.isStatic(field.getModifiers())
336                                        || Modifier.isPrivate(field.getModifiers()))
337                                continue;
338                        Object thisval;
339                        try {
340                                thisval = field.get(this);
341                                final Object thatval = field.get(that);
342                                // If they are both null, or they are equal, continue
343                                if (thisval == null || thatval == null) {
344                                        if (thisval == null && thatval == null)
345                                                continue;
346                                        else
347                                                return false;
348
349                                }
350                                if (thisval.equals(thatval))
351                                        continue;
352
353                        } catch (final Exception e) {
354                                e.printStackTrace();
355                                return false;
356                        }
357                }
358                return true;
359        }
360
361        @Override
362        public USMFStatus clone() {
363                return clone(USMFStatus.class);
364        }
365
366        /**
367         * Clones the tweet to the given class.
368         *
369         * @param <T>
370         * @param clazz
371         * @return a clone of the status
372         */
373        public <T extends USMFStatus> T clone(Class<T> clazz) {
374                return gson.fromJson(gson.toJson(this), clazz);
375        }
376
377        /**
378         * @return get the created_at date as a java date
379         * @throws ParseException
380         */
381        public DateTime createdAt() throws ParseException {
382                final DateTimeFormatter parser = DateTimeFormat
383                                .forPattern("EEE MMM dd HH:mm:ss Z yyyy");
384                if (date == null)
385                        return null;
386                return parser.parseDateTime(date);
387        }
388
389        /**
390         * Container object to hold user information
391         *
392         * @author Laurence Willmore (lgw1e10@ecs.soton.ac.uk)
393         *
394         */
395        public static class User {
396                /**
397                 * User Name
398                 */
399                public String name;
400                /**
401                 * Real name of user
402                 */
403                public String real_name;
404                /**
405                 * Unique User ID
406                 */
407                public double id;
408                /**
409                 * Spoken language of user
410                 */
411                public String language;
412                /**
413                 * UTC time offset of user
414                 */
415                public double utc;
416                /**
417                 * Latitude/Logitude User location
418                 */
419                public double[] geo;
420                /**
421                 * User profile description
422                 */
423                public String description;
424                /**
425                 * Direct href to avatar image
426                 */
427                public String avatar;
428                /**
429                 * Plain Language User location
430                 */
431                public String location;
432                /**
433                 * Number of subscribers
434                 */
435                public double subscribers;
436                /**
437                 * Number of subscriptions
438                 */
439                public int subscriptions;
440                /**
441                 * Number of postings made
442                 */
443                public double postings;
444                /**
445                 * Href to user profile
446                 */
447                public String profile;
448                /**
449                 * Href to user website
450                 */
451                public String website;
452
453                @Override
454                public boolean equals(Object obj) {
455                        if (obj instanceof User) {
456                                final User in = (User) obj;
457                                for (final Field field : User.class.getFields()) {
458                                        try {
459                                                if (field.get(this) == null && field.get(in) == null)
460                                                        continue;
461                                                else if (field.get(this) == null
462                                                                || field.get(in) == null)
463                                                        return false;
464                                                else if (!field.get(this).equals(field.get(in)))
465                                                        return false;
466                                        } catch (final IllegalArgumentException e) {
467
468                                                e.printStackTrace();
469                                        } catch (final IllegalAccessException e) {
470
471                                                e.printStackTrace();
472                                        }
473                                }
474                                return true;
475                        }
476                        return false;
477                }
478
479        }
480
481        /**
482         * Container object for holding link information
483         *
484         * @author Laurence Willmore (lgw1e10@ecs.soton.ac.uk)
485         *
486         */
487        public static class Link {
488                /**
489                 * Title of item
490                 */
491                public String title;
492                /**
493                 * Direct href to thumbnail for item
494                 */
495                public String thumbnail;
496                /**
497                 * Direct href to item
498                 */
499                public String href;
500
501                @Override
502                public boolean equals(Object obj) {
503                        if (obj instanceof Link) {
504                                final Link in = (Link) obj;
505                                for (final Field field : Link.class.getFields()) {
506                                        try {
507                                                if (field.get(this) == null && field.get(in) == null)
508                                                        continue;
509                                                else if (field.get(this) == null
510                                                                || field.get(in) == null)
511                                                        return false;
512                                                else if (!field.get(this).equals(field.get(in)))
513                                                        return false;
514                                        } catch (final IllegalArgumentException e) {
515
516                                                e.printStackTrace();
517                                        } catch (final IllegalAccessException e) {
518
519                                                e.printStackTrace();
520                                        }
521                                }
522                                return true;
523                        }
524                        return false;
525                }
526        }
527
528        @Override
529        public void fillUSMF(USMFStatus status) {
530                status.fillFrom(this);
531        }
532
533        @Override
534        public void fromUSMF(USMFStatus status) {
535                this.fillFrom(status);
536        }
537
538}