enterprises.orbital.evekit.model.CachedData Maven / Gradle / Ivy
package enterprises.orbital.evekit.model;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NoResultException;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.TypedQuery;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import enterprises.orbital.db.ConnectionFactory.RunInTransaction;
import enterprises.orbital.evekit.account.EveKitUserAccountProvider;
import enterprises.orbital.evekit.account.SynchronizedEveAccount;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
/**
* Common abstract class for all model objects. Every synchronized object extends this class. Synchronized objects are immutable with one minor exception. The
* only value that should ever be changed is the lifeEnd field when an object is evolved to a new version.
*/
@Entity
@Inheritance(
strategy = InheritanceType.JOINED)
@Table(
name = "evekit_cached_data",
indexes = {
@Index(
name = "accountIndex",
columnList = "aid",
unique = false),
@Index(
name = "lifeStartIndex",
columnList = "lifeStart",
unique = false),
@Index(
name = "lifeEndIndex",
columnList = "lifeEnd",
unique = false)
})
@JsonIgnoreProperties({
"owner", "accessMask", "metaData", "mask", "allMetaData"
})
@ApiModel(
description = "Model data common properties")
public abstract class CachedData {
private static final Logger log = Logger.getLogger(CachedData.class.getName());
// Limit on number of meta-data tags allowed on a single cached item.
// This is limited to avoid excessive caching of data by third party
// sites
public static final int META_DATA_LIMIT = 10;
// Unique cached data element ID
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "ek_seq")
@SequenceGenerator(
name = "ek_seq",
initialValue = 100000,
allocationSize = 10,
sequenceName = "cached_sequence")
@ApiModelProperty(
value = "Unique ID")
private long cid;
// Account which owns this data
@ManyToOne
@JoinColumn(
name = "aid",
referencedColumnName = "aid")
protected SynchronizedEveAccount owner;
// Version and access mask. These are constants after creation.
// Version 1 - pre Wayback Machine types
// Version 2 - Introduction of wayback machine
@ApiModelProperty(
value = "EveKit release version")
private short eveKitVersion = 2;
private byte[] accessMask;
// About Object Lifelines:
//
// Every cached data object (with a few minor exceptions) has a time when it was created and a time when it was
// deleted. If "start" is the time when an object was created, and "end" is the time when an object was deleted
// then the interval [start, end) is the "life window" in which the object is "visible". Objects are never actually
// deleted from storage. Instead, they are simply not visible outside their life window.
//
// An object is considered immutable within its life window. If it becomes necessary to change the state of an object,
// then the object is copied so that the original's life window terminates at the time of the copy, and the life window
// of the copy starts at the time of the copy. This rule is not strongly enforced. Instead, it is up to the subclass
// to determine when it is appropriate to copy an object and make a new window.
//
// Object mutability is relaxed for meta-data. That is, meta-data may be changed for a given object at any time.
// Meta-data is also copied when an object is evolved. This means that meta-data can never really be referenced
// at a target time. Instead, the underlying object at the given time must be identified first, then the meta data
// for that object can be inspected.
//
// Place in object lifeline:
// [ key.lifeStart, lifeEnd]
@ApiModelProperty(
value = "Model lifeline start (milliseconds UTC)")
protected long lifeStart;
@ApiModelProperty(
value = "Model lifeline end (milliseconds UTC), MAX_LONG for live data")
protected long lifeEnd;
// Transient timestamp fields for better readability
@Transient
@ApiModelProperty(
value = "lifeStart Date")
@JsonProperty("lifeStartDate")
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private Date lifeStartDate;
@Transient
@ApiModelProperty(
value = "lifeEnd Date")
@JsonProperty("lifeEndDate")
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private Date lifeEndDate;
// Object meta data - this will be serialized into storage
@ElementCollection(
fetch = FetchType.EAGER)
private Map metaData = null;
/**
* Update transient date values for readability.
*/
public abstract void prepareDates();
protected Date assignDateField(
long dateValue) {
return dateValue < 0 ? null : new Date(dateValue);
}
/**
* Update transient date values. This method should normally be called by superclasses after updating their own date values.
*/
protected void fixDates() {
lifeStartDate = assignDateField(lifeStart);
lifeEndDate = assignDateField(lifeEnd);
}
public static boolean nullSafeObjectCompare(
Object a,
Object b) {
return a == b || (a != null && a.equals(b));
}
/**
* Initialize this CachedData object.
*
* @param owner
* the owner of this object
* @param start
* start time of the life line of this object
*/
public final void setup(
SynchronizedEveAccount owner,
long start) {
this.owner = owner;
this.lifeStart = start;
this.accessMask = getMask();
this.lifeEnd = Long.MAX_VALUE;
}
/**
* Duplicate the cached data headers of this object onto the target object.
*
* @param target
* the target object to be modified
*/
public final void dup(
CachedData target) {
target.owner = this.owner;
target.eveKitVersion = eveKitVersion;
target.metaData = null;
if (metaData != null) {
target.ensureMetaData();
for (Entry entry : getAllMetaData()) {
target.metaData.put(entry.getKey(), entry.getValue());
}
}
target.accessMask = this.accessMask.clone();
target.lifeStart = this.lifeStart;
target.lifeEnd = this.lifeEnd;
}
/**
* End of life the current Entity (lifeEnd = time), and configure the other entity to be the next generation of the current entity (lifeStart = time, lifeEnd
* = Long.MAX_LONG). Also initializes the CachedData fields of the other entity.
*
* @param other
* object we're evolving to
* @param time
* the time which marks the start of the evolution
*/
public final void evolve(
CachedData other,
long time) {
setLifeEnd(time);
if (other != null) {
dup(other);
other.lifeStart = time;
other.setLifeEnd(Long.MAX_VALUE);
}
}
// Required subclass methods
/**
* Get the access mask for this cached data object.
*
* @return data access mask.
*/
public abstract byte[] getMask();
/**
* Determine whether this entity is equivalent to another entity. Entities only compare their non-CachedData fields for equivalence.
*
* @param other
* the entity to compare against.
* @return true if the entities are equivalent except possibly for their CachedData headers, false otherwise.
*/
public abstract boolean equivalent(
CachedData other);
// Meta-data functions
private void ensureMetaData() {
if (metaData == null) metaData = new HashMap();
}
public String getMetaData(
String key) {
synchronized (this) {
if (metaData == null) return null;
return metaData.get(key);
}
}
public Set> getAllMetaData() {
synchronized (this) {
if (metaData == null) return Collections.emptySet();
return metaData.entrySet();
}
}
public void setMetaData(
String key,
String value)
throws MetaDataLimitException, MetaDataCountException {
if (key == null || key.length() == 0) throw new MetaDataLimitException("Key empty!");
if (value == null) throw new MetaDataLimitException("Value null!");
if (key.length() > 191) throw new MetaDataLimitException("Key too large!");
if (value.length() > 255) throw new MetaDataLimitException("Value too large!");
synchronized (this) {
ensureMetaData();
if (metaData.size() >= META_DATA_LIMIT && !metaData.containsKey(key)) throw new MetaDataCountException("CachedData target has reached MetaData limit!");
metaData.put(key, value);
}
}
public void deleteMetaData(
String key) {
synchronized (this) {
if (metaData != null) metaData.remove(key);
}
}
public boolean hasMetaData() {
synchronized (this) {
return metaData != null && metaData.size() > 0;
}
}
public boolean containsMetaData(
String key) {
synchronized (this) {
return hasMetaData() && metaData.containsKey(key);
}
}
public SynchronizedEveAccount getOwner() {
return owner;
}
public long getCid() {
return cid;
}
public short getEveKitVersion() {
return eveKitVersion;
}
public void setEveKitVersion(
short eveKitVersion) {
this.eveKitVersion = eveKitVersion;
}
public byte[] getAccessMask() {
return accessMask;
}
public void setAccessMask(
byte[] access) {
this.accessMask = access;
}
public void setLifeStart(
long lifeStart) {
this.lifeStart = lifeStart;
}
public long getLifeStart() {
return lifeStart;
}
public long getLifeEnd() {
return lifeEnd;
}
public void setLifeEnd(
long lifeEnd) {
this.lifeEnd = lifeEnd;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(accessMask);
result = prime * result + (int) (cid ^ (cid >>> 32));
result = prime * result + eveKitVersion;
result = prime * result + (int) (lifeEnd ^ (lifeEnd >>> 32));
result = prime * result + (int) (lifeStart ^ (lifeStart >>> 32));
result = prime * result + ((owner == null) ? 0 : owner.hashCode());
return result;
}
@Override
public boolean equals(
Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
CachedData other = (CachedData) obj;
if (!Arrays.equals(accessMask, other.accessMask)) return false;
if (cid != other.cid) return false;
if (eveKitVersion != other.eveKitVersion) return false;
if (lifeEnd != other.lifeEnd) return false;
if (lifeStart != other.lifeStart) return false;
if (owner == null) {
if (other.owner != null) return false;
} else if (!owner.equals(other.owner)) return false;
return true;
}
@Override
public String toString() {
return "CachedData [cid=" + cid + ", owner=" + owner + ", eveKitVersion=" + eveKitVersion + ", accessMask=" + Arrays.toString(accessMask) + ", lifeStart="
+ lifeStart + ", lifeEnd=" + lifeEnd + "]";
}
public static CachedData get(
final long cid,
final String tableName) {
try {
return EveKitUserAccountProvider.getFactory().runTransaction(new RunInTransaction() {
@Override
public CachedData run() throws Exception {
TypedQuery getter = EveKitUserAccountProvider.getFactory().getEntityManager()
.createQuery("SELECT c FROM " + tableName + " c WHERE c.cid = :cid", CachedData.class);
getter.setParameter("cid", cid);
try {
return getter.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
});
} catch (Exception e) {
log.log(Level.SEVERE, "query error", e);
}
return null;
}
public static CachedData get(
final long cid) {
String type = ModelTypeMap.retrieveType(cid);
if (type == null) return null;
return CachedData.get(cid, type);
}
public static A updateData(
final A data) {
try {
return EveKitUserAccountProvider.getFactory().runTransaction(new RunInTransaction() {
@Override
public A run() throws Exception {
A result = EveKitUserAccountProvider.getFactory().getEntityManager().merge(data);
// Ensure type map entry exists
String typeName = data.getClass().getSimpleName();
ModelTypeMap tn = new ModelTypeMap(result.getCid(), typeName);
if (ModelTypeMap.update(tn) == null) return null;
return result;
}
});
} catch (Exception e) {
log.log(Level.SEVERE, "query error", e);
}
return null;
}
public static void cleanup(
final SynchronizedEveAccount toRemove,
final String tableName) {
// Removes all CachedData which refers to this SynchronizedEveAccount. Very dangerous operation. Use with care. We don't use a bulk delete here because we
// need cascading deletes on element collections and the only way to do this (easily) is to let the entity manager handle the removal.
long removeCount = 0;
try {
long lastRemoved = 0;
do {
lastRemoved = EveKitUserAccountProvider.getFactory().runTransaction(new RunInTransaction() {
@Override
public Long run() throws Exception {
long removed = 0;
TypedQuery query = EveKitUserAccountProvider.getFactory().getEntityManager()
.createQuery("SELECT c FROM " + tableName + " c where c.owner = :owner", CachedData.class);
query.setParameter("owner", toRemove);
query.setMaxResults(1000);
for (CachedData next : query.getResultList()) {
EveKitUserAccountProvider.getFactory().getEntityManager().remove(next);
ModelTypeMap.cleanup(next.getCid());
removed++;
}
return removed;
}
});
removeCount += lastRemoved;
} while (lastRemoved > 0);
} catch (Exception e) {
log.log(Level.SEVERE, "query error", e);
}
log.info("Removed " + removeCount + " entities from " + toRemove);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy