package com.wesabe.grendel.entities;
import static com.google.common.base.Objects.*;
import java.io.Serializable;
import java.security.SecureRandom;
import java.util.Set;
import javax.persistence.*;
import javax.ws.rs.core.MediaType;
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.openpgp.MessageReader;
import com.wesabe.grendel.openpgp.MessageWriter;
import com.wesabe.grendel.openpgp.UnlockedKeySet;
import com.wesabe.grendel.util.HashCode;
/**
* A document with an abritrary body, stored as an encrypted+signed OpenPGP
* message.
*
* @author coda
*/
@Entity
@Table(name="documents")
@IdClass(DocumentPK.class)
@NamedQueries({
@NamedQuery(
name="com.wesabe.grendel.entities.Document.ByOwnerAndName",
query="SELECT d FROM Document AS d WHERE d.name = :name AND d.owner = :owner"
)
})
public class Document implements Serializable {
private static final long serialVersionUID = 5699449595549234402L;
@Id
private String name;
@Id
private User owner;
@Column(name="content_type", nullable=false, length=40)
private String contentType;
@Column(name="body", nullable=false)
@Lob
private byte[] body;
@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;
@ManyToMany(fetch=FetchType.LAZY, mappedBy="linkedDocuments", cascade={CascadeType.ALL})
@JoinTable(name="links")
private Set<User> linkedUsers = Sets.newHashSet();
@Version
@Column(name="version", nullable=false)
private long version = 0;
@Deprecated
public Document() {
// for Hibernate usage only
}
/**
* Creates a new {@link Document}, owned by the given {@link User} and with
* the given name.
*
* @param owner the new document's owner
* @param name the new document's name
* @param contentType the document's content type
*/
public Document(User owner, String name, MediaType contentType) {
this.owner = owner;
this.name = name;
this.contentType = contentType.toString();
this.createdAt = new DateTime(DateTimeZone.UTC);
this.modifiedAt = new DateTime(DateTimeZone.UTC);
}
/**
* Returns the document's owner.
*/
public User getOwner() {
return owner;
}
/**
* Returns the document's name.
*/
public String getName() {
return name;
}
/**
* Returns a set of {@link User}s who have read-only access to this
* document.
*/
public Set<User> getLinkedUsers() {
return linkedUsers;
}
/**
* Provide a {@link User} with read-only access to this document.
*/
public void linkUser(User user) {
linkedUsers.add(user);
user.getLinkedDocuments().add(this);
}
/**
* Remove a {@link User}'s read-only access to this document.
*/
public void unlinkUser(User user) {
linkedUsers.remove(user);
user.getLinkedDocuments().remove(this);
}
/**
* Returns {@code true} if the given {@link User} has read-only access to
* this document.
*/
public boolean isLinked(User user) {
return linkedUsers.contains(user);
}
/**
* Returns the document's content type.
*/
public MediaType getContentType() {
return MediaType.valueOf(contentType);
}
/**
* Returns a UTC timestamp of when this document was created.
*/
public DateTime getCreatedAt() {
return toUTC(createdAt);
}
/**
* Sets a UTC timestamp of when this document was created.
*/
public void setCreatedAt(DateTime createdAt) {
this.createdAt = toUTC(createdAt);
}
/**
* Returns a UTC timestamp of when this document was last modified.
*/
public DateTime getModifiedAt() {
return toUTC(modifiedAt);
}
/**
* Sets a UTC timestamp of when this document was last modified.
*/
public void setModifiedAt(DateTime modifiedAt) {
this.modifiedAt = toUTC(modifiedAt);
}
/**
* Sets the {@link Document}'s body to an encrypted+signed OpenPGP message
* containing {@code body}.
*
* @param keySet
* the {@link UnlockedKeySet} of the {@link User} that owns this
* {@link Document}
* @param random
* a {@link SecureRandom} instance
* @param body
* the unencrypted document body
* @throws CryptographicException
* if the owner's {@link KeySet} cannot be unlocked with {@code
* ownerPassphrase}
* @see MessageWriter
*/
public void encryptAndSetBody(UnlockedKeySet keySet, SecureRandom random,
byte[] body) throws CryptographicException {
final Set<KeySet> recipients = Sets.newHashSetWithExpectedSize(linkedUsers.size());
for (User linkedUser : linkedUsers) {
recipients.add(linkedUser.getKeySet());
}
final MessageWriter writer = new MessageWriter(keySet, recipients, random);
this.body = writer.write(body);
}
/**
* Decrypts the document's body using the {@link UnlockedKeySet} of the
* owner or a recipient;
*
* @param unlockedKeySet
* an {@link UnlockedKeySet} belonging to either the
* {@link Document}'s owner or a recipient
* @return the decrypted document body
* @throws CryptographicException
* if there is an error decrypting and verifying the
* encrypted+signed OpenPGP message
* @see MessageReader
*/
public byte[] decryptBody(UnlockedKeySet unlockedKeySet) throws CryptographicException {
final MessageReader reader = new MessageReader(owner.getKeySet(), unlockedKeySet);
return reader.read(body);
}
private DateTime toUTC(DateTime dateTime) {
return dateTime.toDateTime(DateTimeZone.UTC);
}
@Override
public int hashCode() {
return HashCode.calculate(
getClass(), body, contentType, createdAt, modifiedAt, name, owner
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Document)) {
return false;
}
final Document that = (Document) obj;
return equal(name, that.name) && equal(owner, that.owner) &&
equal(body, that.body) && equal(createdAt, that.createdAt) &&
equal(contentType, that.contentType) &&
equal(modifiedAt, that.modifiedAt);
}
@Override
public String toString() {
return name;
}
/**
* Returns an opaque string indicating the {@link Document}'s name and
* version.
*/
public String getEtag() {
return new StringBuilder("doc-").append(name).append('-').append(version).toString();
}
}