/* Copyright 2014 MITRE Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mitre.provenance.plusobject; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.mitre.provenance.Metadata; import org.mitre.provenance.PLUSException; import org.mitre.provenance.PropertyCapable; import org.mitre.provenance.PropertySet; import org.mitre.provenance.client.ProvenanceClient; import org.mitre.provenance.dag.DAGPath; import org.mitre.provenance.dag.LineageDAG; import org.mitre.provenance.mediator.Mediator; import org.mitre.provenance.surrogate.SignPost; import org.mitre.provenance.surrogate.SurrogateGeneratingFunction; import org.mitre.provenance.surrogate.Surrogateable; import org.mitre.provenance.tools.PLUSUtils; import org.mitre.provenance.user.PrivilegeSet; import org.mitre.provenance.user.User; /** * A generic PLUSObject. While this object provides lots of functionality, you * never should instantiate *just* a PLUSObject, but you should use the * subtypes. See the load() method and the extentLoaderMappings for details on * how this works. * <p> * PLUSObjects may be surrogates. (Check via the isSurrogate() method). * Surrogates cannot be written to the database, or loaded from the database, * they must be computed. (Check computeSurrogates()) * <p> * All PLUSObjects have "types" and "subtypes". The major type would be * something like "workflow", "invocation", or "data". The subtype is used by * some subclass of PLUSObject to be more specific about what kind of object it * is. So you could have a subtype of "table" under the type "data". And so on. * Types are used to determine which subclass is best suited to load the * PLUSObject. (Check extentLoaderMappings for more details) * * @author moxious * */ public class PLUSObject extends Surrogateable implements Comparable<Object>, PropertyCapable, Cloneable, SourcedObject { protected static Logger log = Logger.getLogger(PLUSObject.class.getName()); public static final String PLUS_TYPE_WORKFLOW = "workflow"; public static final String PLUS_TYPE_DATA = "data"; public static final String PLUS_TYPE_ACTIVITY = "activity"; public static final String PLUS_TYPE_HERITABLE = "heritable"; public static final String PLUS_TYPE_INVOCATION = "invocation"; public static final String PLUS_TYPE_GENERIC = "generic"; /** The location of the running PLUS prototype. */ public static final String PLUS_NAMESPACE = "http://denim.mitre.org/plus/"; /** Unique identifier for a provenance object */ protected String id; /** Object's name */ protected String name; /** Milliseconds since the epoch measurement of when the object was created */ protected long created; /** Optional uncertainty (default 1.0) */ protected Float uncertainty = (float) 1.0; /** A type identifier for what kind of provenance object this is */ protected PLUSObjectType type; /** Source hints for this object */ protected SignPost srcHints = SignPost.SRC_HINTS_LOCAL; /** Link to the PLUSActor who owns this */ protected PLUSActor owner; /** List of privileges required to see this object */ protected PrivilegeSet privileges; /** Metadata fields associated with the object */ protected Metadata metadata; /** Create an empty blank PLUSObject. An ID will be automatically generated */ public PLUSObject() { super(); setName("Unnamed"); setId(PLUSUtils.generateID()); setCreated(); metadata = new Metadata(); type = new PLUSObjectType("object", "object"); privileges = new PrivilegeSet(); } /** Create a plus object with a particular name */ public PLUSObject(String name) { this(); type = new PLUSObjectType("unknown", "unknown"); setName(name); privileges = new PrivilegeSet(); } // End constructor /** Create a PLUSObject that is a copy of the other */ public PLUSObject(PLUSObject other) { this(); copy(other); } /** * Copies all information from the specified PLUS object to this PLUS object, with * one exception: type/subtype information is copied ONLY IF this object is lacking that * information. */ public void copy(PLUSObject other) { setMetadata(other.getMetadata()); setName(other.getName()); setSGFs(other.getSGFs()); setCreated(other.getCreated()); setId(other.getId()); setPrivileges(other.getPrivileges()); setUncertainty(other.getUncertainty()); setOwner(other.getOwner()); setSourceHints(other.getSourceHints()); // TODO // WARN: Sometimes it's desirable to copy these fields, but remember that // it fundamentally changes what the object is. So this has been changed // to copy type/subtype only when they're empty. if(getObjectType() == null) setObjectType(other.getObjectType()); if(getObjectSubtype() == null) setObjectSubtype(other.getObjectSubtype()); } // End copy public String getId() { return id; } public String getName() { return name; } public Date getCreatedAsDate() { return new java.util.Date(created); } /** * Get the date/time when this object was created. * @return Creation time, in milliseconds since the epoch. (midnight, January 1, 1970 UTC) * @see PLUSObject#getCreatedAsDate() */ public long getCreated() { return created; } public Float getUncertainty() { return uncertainty; } public String getObjectType() { return type.getObjectType(); } public String getObjectSubtype() { return type.getObjectSubtype(); } /** @see SourcedObject#getSourceHints() */ public SignPost getSourceHints() { return srcHints; } /** * Get the privilege set associated with this object, i.e. the privileges users must have in order to * access/see this object. */ public PrivilegeSet getPrivileges() { return privileges; } public Metadata getMetadata() { return metadata; } public PLUSObjectType getType() { return type; } /** Returns the certainty of the PLUS object */ public String getCertainty() { NumberFormat df = NumberFormat.getNumberInstance(); df.setMaximumFractionDigits(2); return df.format(100.0 * uncertainty) + "%"; } public void setMetadata(Metadata metadata) { this.metadata = metadata; } public void setName(String name) { this.name = name; } public void setPrivileges(PrivilegeSet ps) { privileges = ps; } public void setId(String id) { this.id = id; } /** @see SourcedObject#setSourceHints(SignPost) */ public void setSourceHints(SignPost sp) { srcHints = sp; } /** Set the object's created time, in milliseconds since the epoch */ public void setCreated(long d) { created = d; } /** * Sets the object's created timestamp to this moment in milliseconds since the epoch, UTC */ public void setCreated() { Calendar calInitial = Calendar.getInstance(); int offsetInitial = calInitial.get(Calendar.ZONE_OFFSET) + calInitial.get(Calendar.DST_OFFSET); long current = System.currentTimeMillis(); // Check right time created = current - offsetInitial; } public void setOwner(PLUSActor owner) { this.owner = owner; } protected void setType(PLUSObjectType type) { this.type = type; } protected void setObjectSubtype(String objectSubtype) { this.type.setObjectSubtype(objectSubtype); } protected void setObjectType(String objectType) { this.type.setObjectType(objectType); } /** Set the uncertainty for the PLUS object */ public void setUncertainty(Float uncertainty) { if(uncertainty == null) this.uncertainty = (float)1; else this.uncertainty = uncertainty > 1 ? 1 : uncertainty < 0 ? 0 : uncertainty; } /** * Determines whether the property or meaning of this node is "heritable" to * others via FLING. Almost always, the answer is false. */ public boolean isHeritable() { return type.getObjectType().equals("heritable"); } public boolean isDataItem() { return type.getObjectType().equals("data"); } public boolean isActivity() { return type.getObjectType().equals("activity"); } public boolean isInvocation() { return type.getObjectType().equals("invocation"); } public boolean isWorkflow() { return type.getObjectType().equals("workflow"); } /** * Get the PLUSActor who is considered the owner of this object. * @return the PLUSActor, or null if there is no known/identified actor. */ public PLUSActor getOwner() { return owner; /* String aid = getOwnerId(); if (aid == null || "".equals(aid)) return null; log.finest("PLUSObject#getOwner: Loading actor " + getOwnerId() + " from database"); try { Node n = Neo4JStorage.exists(this); if(n == null) throw new PLUSException(toString() + " hasn't been written yet"); // Assumption: only one actor owns each relationship. This will throw an exception // if that turns out to be wrong. Relationship r = n.getSingleRelationship(Neo4JStorage.OWNS, Direction.INCOMING); if(r == null) { log.warning("Can't load PLUSActor because relationship is null, but aid=" + aid); return null; } else if(r.getStartNode() == null) { log.warning("Can't load PLUSActor because OWNS relationship start node is null"); return null; } PLUSActor owner = Neo4JPLUSObjectFactory.newActor(r.getStartNode()); if(owner == null) { log.warning("After loading node " + r.getStartNode() + " PLUSActor is null"); return null; } // Added for integrity checking, but this should never happen. Paranoid coding. if(!aid.equals(owner.getId())) throw new PLUSException("Found actor " + owner + " doesn't match claimed " + aid); return owner; } catch (Exception e) { log.severe("Failed to load actor: " + e.getMessage()); return null; } */ } // End getOwner /** * Given a class, return a new instance object using the reflection API * * @param c the fully qualified name of the SGF class (i.e. org.mitre.provenance.surrogate.BlahBlahBlah) * @return a new instance of that class. * @throws IllegalAccessException * @throws InstantiationException * @throws ClassNotFoundException * @throws Exception */ private SurrogateGeneratingFunction getSGFInstance(String c) throws InstantiationException, IllegalAccessException, ClassNotFoundException { // log.info("getSGFInstance: " + c); Class<?> clazz = Class.forName(c); if(!SurrogateGeneratingFunction.class.isAssignableFrom(clazz)) { log.severe("SGF isn't assignable from " + c + "!"); } SurrogateGeneratingFunction f = (SurrogateGeneratingFunction) clazz.newInstance(); return f; } // End getSGFInstance public PLUSObject clone() { PLUSObject c = new PLUSObject(this); return c; } // End clone /** * Gets a version of this object that is suitable for a given user. * The mediator may be invoked if more than one SGF is present, to choose the best available choice. * @see org.mitre.provenance.mediator.Mediator * @param user the user asking to see the object. * @return the object itself (if the user is permitted to see it) a suitable surrogate (if applicable) * or null (if the user is not permitted to see this object) * @throws PLUSException if user is null */ public PLUSObject getVersionSuitableFor(User user) throws PLUSException { if(user == null) throw new PLUSException("Must specify user!"); if(ProvenanceClient.instance.canSee(user, this)) return this; List<PLUSObject> surrogates = computeSurrogates(user); if(surrogates == null || surrogates.size() == 0) { log.info("No suitable surrogates of " + getId() + " for " + user.getName()); return null; } Mediator m = new Mediator(); return m.getMostPreferable(surrogates); } /** * Use the SGFs attached to this object and generate relevant surrogates. * * @return an array of PLUSObjects that are suitable surrogates for u. * @throws PLUSException */ public ArrayList<PLUSObject> computeSurrogates(User u) throws PLUSException { ArrayList<PLUSObject> surrogates = new ArrayList<PLUSObject>(); // log.debug("***** Computing surrogates for " + this); System.out.flush(); if (sgfs == null) { log.severe("computeSurrogates: No SGFs!"); sgfs = new ArrayList<String>(); } for (int x = 0; x < sgfs.size(); x++) { String c = sgfs.get(x); try { // log.debug("Computing surrogate for " + getName() + // " with " + c); System.out.flush(); SurrogateGeneratingFunction f = getSGFInstance(c); if (!f.isRunnable(this)) { log.warning("Skipping SGF " + f + " because it isn't ready."); continue; } PLUSObject surrogate = f.generateSurrogate(this, u); if (surrogate == null) { log.warning("SGF returned null! Skipping..."); continue; } // It's important that the surrogate has the same ID as the // object that it's // standing in for. Otherwise BLING and FLING on a surrogate // won't work. if (!getId().equals(surrogate.getId())) { log.warning("Surrogate function " + c + " did not properly set surrogate ID - force fixing."); surrogate.setId(getId()); } // End if if (!surrogate.isSurrogate()) { System.err.println("Surrogate function " + c + " did not properly mark its result as a surrogate " + "on item " + getName() + "/" + getId()); throw new PLUSException("SGF " + f + " returned a non-surrogate!"); } // End if if (!this.getType().compatibleWith(surrogate.getType())) { // log.warning("WARNING: Surrogate function " + c // + " returned an incompatible type on item " // + getName() + "/" + getId() + "! Original type was " + this.getType()); // TODO Should this throw an exception or not??? Sample SGF // functions actually do // this, so be prepared to go fix them... // throw new PLUSException("SGF " + f + // " returned incompatible type!"); } // End if surrogates.add(surrogate); } catch (Exception e) { log.severe("PLUSObject#computeSurrogate: surrogate function threw: " + e.getMessage()); throw new PLUSException("Error computing surrogate: " + e, e); } // End catch } // End for return surrogates; } // End getSurrogates /** * Determine whether this PLUSObject is reachable from another PLUSObject * within a particular DAG. * * @param otherNode * the other object * @param dag * the DAG to use as context * @return true if there is a FLING path from otherNode to this object * within the given DAG. False otherwise. * @throws PLUSException */ public boolean reachableFrom(PLUSObject otherNode, LineageDAG dag) { return DAGPath.pathExists(dag, otherNode, this); } // End reachableFrom /** * Determine whether this PLUSObject can reach another PLUSObject within a * particular DAG. * * @param otherNode * the other object * @param dag * the DAG to use as context * @return true if there is a FLING path from this node to otherNode within * a given DAG. False otherwise. */ public boolean canReach(PLUSObject otherNode, LineageDAG dag) { return DAGPath.pathExists(dag, this, otherNode); } public boolean createdBefore(PLUSObject other) { return getCreated() < other.getCreated(); } public boolean createdAfter(PLUSObject other) { return getCreated() > other.getCreated(); } public String toString() { String foo = new String(getName() + "/" + getUncertainty() + "/" + getObjectType() + "/" + getObjectSubtype()); if (isSurrogate()) return "SURROGATE: " + foo; else return foo; } // End toString /** * Get a URI string that uniquely identifies this object. * * @return a URI */ public String getURI(String contextPath) { return PLUSObject.oidToURI(getId(), contextPath); } public int compareTo(Object arg0) { return toString().compareTo("" + toString()); } // End compareTo /** * Given a unique ID, return the URL where it can be located. This is * install-specific. * * @param oid * unique ID of a PLUSObject * @return a URL where it can be found. * @see PLUSUtils#NAMESPACE */ public static String oidToURI(String oid, String contextPath) { return contextPath + "/graph/graphPage.jsp?oid=" + oid; } public Map<String,Object> getStorableProperties() { HashMap<String,Object> m = new HashMap<String,Object>(); m.put("oid", getId()); m.put("name", getName()); m.put("created", getCreated()); /* * TODO verify removal of these properties. m.put("ownerid", null); if(getOwner() != null) { m.put("ownerid", getOwner().getId()); } */ m.put("type", getObjectType()); m.put("subtype", getObjectSubtype()); m.put("certainty", getCertainty()); if(sgfs != null && sgfs.size() > 0) { String [] sgfClassNames = new String[sgfs.size()]; for(int x=0; x<sgfs.size(); x++) { sgfClassNames[x] = sgfs.get(x); } m.put("SGFs", sgfClassNames); } return m; } public PLUSObject setProperties(PropertySet props, ProvenanceCollection contextCollection) throws PLUSException { String [] requiredProps = new String [] { "oid", "name", "type", "subtype" }; for(int x=0; x<requiredProps.length; x++) { if(props.getProperty(requiredProps[x], null) == null) throw new PLUSException("Required property " + requiredProps[x] + " missing"); } setId(""+props.getProperty("oid")); setName(""+props.getProperty("name")); setCreated((Long)props.getProperty("created")); // TODO // The "ownerid" field is a holdover from when the data was relational. It probably shouldn't be stored at all, // in favor of a relationship to an actor node, which is definitely already there in the storage model. Still, // this needs careful consideration and checking before it's removed as a property. String aid = (String)props.getProperty("ownerid", null); if(aid != null && !"".equals(aid) && !"null".equals(aid)) { if(contextCollection != null && contextCollection.containsActorID(aid)) setOwner(contextCollection.getActor(aid)); else { // log.severe("Cannot set owner for object: aid " + aid + " isn't in the context collection " + contextCollection); setOwner(null); } } else setOwner(null); setObjectType((String)props.getProperty("type", null)); setObjectSubtype((String)props.getProperty("subtype", null)); Object unc = props.getProperty("uncertainty", null); String sunc = ""+unc; if(unc == null || "null".equals(sunc) || "".equals(sunc)) setUncertainty(null); else setUncertainty(Float.parseFloat(sunc)); String[] sgfs = (String[])props.getProperty("SGFs", null); if(sgfs != null) { for(String className : sgfs) { try { SurrogateGeneratingFunction sgf = getSGFInstance(className); useSurrogateComputation(sgf); } catch(ClassNotFoundException cnfe) { log.severe("No such class " + className); throw new PLUSException("Missing/bad SGF " + className, cnfe); } catch(Exception exc) { log.severe("Couldn't instantiate SGF " + className + ": " + exc.getMessage()); } } } Metadata m = new Metadata(); String tok = "metadata:"; for(String k : props.getPropertyKeys()) { if(k.indexOf(tok) >= 0) m.put(k.substring(tok.length()), props.getProperty(k)); } // End for setMetadata(m); return this; } } // End PLUSObject