package models;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import controllers.PrivateWeb;
import misc.C2DM;
import misc.ValidationError;
import play.data.validation.Required;
import play.data.validation.Validation;
import play.data.validation.Validation.ValidationResult;
import play.db.jpa.Model;
import play.libs.Crypto;
@Entity
public class User extends Model {
@Required
public String username;
public String socialId;
@Required
public String password; // password = hash ( inputPassword + salt )
@Required
public String salt;
@Required
public String email;
public String cellphone;
public byte[] avatar;
@Required
public Long score;
public String c2dmID;
@OneToMany(mappedBy="user")
public List<Report> reports;
@OneToMany(mappedBy="receiver")
public List<Notification> notifications;
@OneToMany(mappedBy="reporter")
public List<Notification> notificationsCausedByMe;
@OneToMany(mappedBy="user", cascade=CascadeType.ALL)
public List<Park> parks;
@ManyToMany(mappedBy="users")
public List<Badge> badges;
@ManyToMany
public List<Report> reportsAffectingMe;
@Required
public Date registrationDate;
public static final long POINTS_FOR_PARK = -1L;
public static final long POINTS_FOR_REPORT = 2L;
public static final long POINTS_FOR_CONFIRM = 4L;
public static final long POINTS_FOR_DENY = -4L;
protected static final SecureRandom random = new SecureRandom(); // it is thread-safe
public static String hashPassword(String inputPassword, String salt) {
return Crypto.passwordHash(inputPassword + salt);
}
public User(String username, String password, String email,
String cellphone, byte[] avatar) {
this.username = username;
this.salt = new BigInteger(64, random).toString(32);
this.password = hashPassword(password, this.salt);
this.email = email;
this.cellphone = cellphone;
this.reports = new ArrayList<Report>();
this.parks = new ArrayList<Park>();
this.badges = new ArrayList<Badge>();
this.reportsAffectingMe = new ArrayList<Report>();
this.avatar = avatar;
this.score = 20L;
this.registrationDate = new Date();
this.notifications = new ArrayList<Notification>();
this.notificationsCausedByMe = new ArrayList<Notification>();
}
public Long getScore() {
return score;
}
public static boolean connect(String username, String passwordAttempted) {
User u = User.find("byUsername",username).first();
if (u == null) {
return false;
}
return u.authenticate(passwordAttempted);
}
protected boolean authenticate(String passAttempted) {
return hashPassword(passAttempted, this.salt).equals(this.password);
}
public Token generateToken() {
return new Token(this).save();
}
public void parkCar(double latitude, double longitude) {
Park park = new Park(latitude, longitude, new Date(), this).save();
this.parks.add(park);
this.score += POINTS_FOR_PARK;
this.save();
}
public void unpark() {
if (this.parks.size() == 0) {
return;
}
Park park = this.parks.get(this.parks.size() - 1);
park.removeCar();
park.save();
this.save();
}
public void report(double latitude, double longitude, String type) {
Report r = new Report(latitude, longitude, System.currentTimeMillis(), type, this);
r.save();
this.reports.add(r);
List<User> users = User.findAll();
boolean helpedUsers = false;
for (User user : users) {
if (user.newReportDetected(r)) {
helpedUsers = true;
}
}
if (helpedUsers) {
this.score += POINTS_FOR_REPORT;
}
this.save();
}
// returns a potential error
public static ValidationError createNormalUser(Validation validation, String username, String password, String email, String cellphone) {
if (username == null || username.length() == 0) {
return ValidationError.USERNAME_EMPTY;
}
if (password == null || password.length() == 0) {
return ValidationError.PASSWORD_EMPTY;
}
ValidationResult emailValidation = validation.email(email);
if (!emailValidation.ok) {
return ValidationError.EMAIL_INCORRECT;
}
if(cellphone != null && cellphone.length() != 0) {
ValidationResult phoneValidation = validation.phone(cellphone);
if (!phoneValidation.ok) {
return ValidationError.PHONE_NUMBER_INCORRECT;
}
}
User newUser = User.find("byUsername",username).first();
if (newUser != null) {
return ValidationError.USERNAME_ALREADY_EXISTS;
}
newUser = new User(username, password, email, cellphone, null);
newUser.save();
return null;
}
public static User findOrCreateSocialUser(String socialId, String name, String email) {
User user = User.find("bySocialId", socialId).first();
if (user == null) {
// TODO get the user cellphone
user = new User(name, "", email, null, null);
user.socialId = socialId;
user.save();
}
return user;
}
public static User findOrCreateSocialUser(String socialId, String name) {
User user = User.find("bySocialId", socialId).first();
if (user == null) {
// TODO get the user cellphone
user = new User(name, "", null, null, null);
user.socialId = socialId;
user.save();
}
return user;
}
protected static final double MAX_DISTANCE_FOR_WARNING = 750;
protected static final long MAX_TIME_RECENT_REPORTS = 1000 * 60 * 60;
// subtract 1 hour from the time of the request
public List<Report> fetchNewReportsSince(long requestDate, boolean subtract) {
long deadline = requestDate - (subtract ? MAX_TIME_RECENT_REPORTS : 0);
List<Report> newReports = new ArrayList<Report>();
int parksSize = this.parks.size();
Park park = (parksSize > 0 ? this.parks.get(parksSize - 1) : null);
if (park == null || park.removed) {
return newReports;
}
int sizeReports = reportsAffectingMe.size();
// Traverse backwards because the reports are laid out in the list in increasing time order
for (int i = sizeReports - 1; i >= 0; i--) {
Report affectingMe = reportsAffectingMe.get(i);
if (affectingMe.date >= deadline && distanceBetweenCoords(park.latitude, park.longitude, affectingMe.latitude, affectingMe.longitude) <= MAX_DISTANCE_FOR_WARNING) {
newReports.add(affectingMe);
long timePassed = requestDate - affectingMe.date;
affectingMe.fade = (100.0f - ((timePassed * 100.0f) / MAX_TIME_RECENT_REPORTS)) / 100.0f;
} else {
break;
}
}
return newReports;
}
public boolean newReportDetected(Report report) {
int parksSize = this.parks.size();
if (parksSize == 0) {
return false;
}
Park park = this.parks.get(parksSize - 1);
if (park.removed) {
return false;
}
if (distanceBetweenCoords(park.latitude, park.longitude, report.latitude, report.longitude) > MAX_DISTANCE_FOR_WARNING) {
return false;
}
this.reportsAffectingMe.add(report);
this.save();
report.affectedSomeUser = true;
report.save();
// TODO spawn a thread or use a thread pool to execute batch jobs (most likely play
// offers some utility for this?) to process the report and make the notifications
if(this.c2dmID!=null) {
new C2DM().sendMessage(report, this, "Reportado um funcionário perto do seu carro");
}
return true;
}
protected static double distanceBetweenCoords(double lat1, double lng1, double lat2, double lng2) {
double earthRadius = 6367;
double dLat = Math.toRadians(lat2-lat1);
double dLng = Math.toRadians(lng2-lng1);
double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(Math.toRadians(lat1)) *
Math.cos(Math.toRadians(lat2)) *
Math.sin(dLng/2) * Math.sin(dLng/2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
double dist = earthRadius * c;
return dist * 1000;
}
}