package com.wesabe.grendel.entities;
import static com.google.common.base.Objects.*;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.*;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import com.google.common.collect.Sets;
import com.wesabe.grendel.openpgp.CryptographicException;
import com.wesabe.grendel.openpgp.KeySet;
import com.wesabe.grendel.util.HashCode;
/**
* A Grendel user.
*
* @author coda
*/
@Entity
@Table(name="users")
@NamedQueries({
@NamedQuery(
name="com.wesabe.grendel.entities.User.Exists",
query="SELECT u.id FROM User AS u WHERE u.id = :id"
),
@NamedQuery(
name="com.wesabe.grendel.entities.User.All",
query="SELECT u FROM User AS u ORDER BY u.id"
)
})
public class User implements Serializable {
private static final long serialVersionUID = -8270919660085011028L;
@Id
@Column(name="id")
private String id;
@Column(name="keyset", nullable=false)
@Lob
private byte[] encodedKeySet;
@Transient
private KeySet keySet = null;
@Column(name="created_at", nullable=false)
@Type(type="org.joda.time.contrib.hibernate.PersistentDateTime")
private DateTime createdAt;
@Column(name="modified_at", nullable=false)
@Type(type="org.joda.time.contrib.hibernate.PersistentDateTime")
private DateTime modifiedAt;
// FIXME coda@wesabe.com -- Dec 27, 2009: User#documents double-loads document primary keys.
// This may be a bug in Hibernate, but the SQL this generates produces
// queries which look like this:
// documents0_.owner_id as owner6_1_,
// documents0_.name as name1_,
// documents0_.name as name1_0_,
// documents0_.owner_id as owner6_1_0_,
// documents0_.body as body1_0_,
// documents0_.content_type as content3_1_0_,
// documents0_.created_at as created4_1_0_,
// documents0_.modified_at as modified5_1_0_
//
// I've tried just about every approach to composite keys in Hibernate that
// I found, and none of them changed this behavior.
@OneToMany(mappedBy="owner", fetch=FetchType.LAZY, cascade={CascadeType.ALL})
@OnDelete(action=OnDeleteAction.CASCADE)
private Set<Document> documents = Sets.newHashSet();
@ManyToMany(fetch=FetchType.LAZY, cascade={CascadeType.ALL})
@ForeignKey(
name="FK_LINK_TO_USER",
inverseName="FK_LINK_TO_DOCUMENT"
)
@JoinTable(
name="links",
joinColumns={
@JoinColumn(name="user_id", nullable=false, referencedColumnName="id")
},
inverseJoinColumns={
@JoinColumn(name="document_name", nullable=false, referencedColumnName="name"),
@JoinColumn(name="document_owner_id", nullable=false, referencedColumnName="owner_id")
}
)
// FIXME coda@wesabe.com -- Dec 31, 2009: Fix links's ON CASCADE actions.
// The foreign-key constraint FK_LINK_TO_USER and FK_USER_TO_LINK don't work
// with this @OnDelete annotation or the one on Document#linkedUsers, which
// means there's a potential consistency problem. The code *should* take
// care of that, but it's still a worry. Apparently this is a long-standing
// bug/deficiency with Hibernate's @OnDelete:
// http://opensource.atlassian.com/projects/hibernate/browse/HHH-4404
// @OnDelete(action=OnDeleteAction.CASCADE)
private Set<Document> linkedDocuments = Sets.newHashSet();
@Version
@Column(name="version", nullable=false)
private long version = 0;
@Deprecated
public User() {
// blank constructor to be used by Hibernate
}
/**
* Creates a new Grendel user with a given {@link KeySet}.
*
* @param keySet the {@link KeySet} belonging to the user
*/
public User(KeySet keySet) {
setKeySet(keySet);
this.createdAt = new DateTime(DateTimeZone.UTC);
this.modifiedAt = new DateTime(DateTimeZone.UTC);
}
/**
* Returns the user's id.
*/
public String getId() {
return id;
}
/**
* Returns the user's {@link KeySet}.
*/
public KeySet getKeySet() {
if (keySet == null) {
try {
this.keySet = KeySet.load(encodedKeySet);
} catch (CryptographicException e) {
throw new RuntimeException(e);
}
}
return keySet;
}
/**
* Replaces the user's {@link KeySet} with another.
*
* @param keySet a new {@link KeySet}
*/
public void setKeySet(KeySet keySet) {
this.keySet = keySet;
this.id = keySet.getUserID();
this.encodedKeySet = keySet.getEncoded();
}
/**
* Returns a UTC timestamp of when this user was created.
*/
public DateTime getCreatedAt() {
return toUTC(createdAt);
}
/**
* Sets a UTC timestamp of when this user was created.
*/
public void setCreatedAt(DateTime createdAt) {
this.createdAt = toUTC(createdAt);
}
/**
* Returns a UTC timestamp of when this user was last modified.
*/
public DateTime getModifiedAt() {
return toUTC(modifiedAt);
}
/**
* Sets a UTC timestamp of when this user was last modified.
*/
public void setModifiedAt(DateTime modifiedAt) {
this.modifiedAt = toUTC(modifiedAt);
}
/**
* Returns a set of the user's {@link Document}s.
*/
public Set<Document> getDocuments() {
return documents;
}
/**
* Returns a set of the user's linked {@link Document}s.
*/
public Set<Document> getLinkedDocuments() {
return linkedDocuments;
}
private DateTime toUTC(DateTime dateTime) {
return dateTime.toDateTime(DateTimeZone.UTC);
}
@Override
public int hashCode() {
return HashCode.calculate(createdAt, encodedKeySet, id, modifiedAt);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof User)) {
return false;
}
final User that = (User) obj;
return equal(id, that.id) && equal(encodedKeySet, that.encodedKeySet) &&
equal(createdAt, that.createdAt) && equal(modifiedAt, that.modifiedAt) &&
equal(encodedKeySet, that.encodedKeySet);
}
@Override
public String toString() {
return id;
}
/**
* Returns an opaque string indicating the {@link User}'s name and
* version.
*/
public String getEtag() {
return new StringBuilder("user-").append(id).append('-').append(version).toString();
}
}