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}