/* * Part of the CCNx Java Library. * * Copyright (C) 2008, 2009, 2013 Palo Alto Research Center, Inc. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. You should have received * a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301 USA. */ package org.ccnx.ccn.io.content; import java.io.IOException; import java.io.InputStream; import java.io.InvalidObjectException; import java.io.OutputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.logging.Level; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.ErrorStateException; import org.ccnx.ccn.io.NullOutputStream; /** * A NetworkObject provides support for storing an object in a network based backing store. * It provides support for loading the object from the network, tracking if the object's data * has been changed, to determine whether it needs to be saved or not and saving the object. * * It can have 3 states: * - available: refers to whether it has data (either set by caller or updated from network) * (potentiallyDirty refers to whether it has been saved since last set; it might not * actually be dirty if saved to same value as previous) * - stored: saved to network or updated from network and not since saved * * It can be: * - not available (no data assigned, not saved or read, basically not ready) * - available but not stored (assigned locally, but not yet stored anywhere; this * means that storage-related metadata is unavailable even though data can be read * back out), or assigned locally since last stored * - if assigned locally but unchanged, it will not be rewritten and last stored * metadata * - available and stored (stored by caller, or read from network) * * Subclasses can vary as to whether they think null is valid data for an object -- i.e. * whether assigning the object's value to null makes it available or not. The default behavior * is to not treat a null assignment as a value -- i.e. not available. */ public abstract class NetworkObject<E> { /** * Care about speed, not collision-resistance. */ public static final String DEFAULT_CHECKSUM_ALGORITHM = "MD5"; protected Class<E> _type; protected E _data; protected boolean _isDirty = false; protected boolean _isPotentiallyDirty = false; /** * Is it possible to modify the type of data we contain directly * from a pointer to the object, or do we have to replace the whole * thing to change its value? For example, a Java String or BigInteger * is immutable (modulo reflection-based abstraction violations). A * complex structure whose fields can be set would be mutable. We want * to track whether the content of the object has been changed either * using setData or outside of the object interface; this is an * optimization to allow us to avoid the outside-the-object checks * for immutable objects. */ protected boolean _contentIsMutable = false; protected byte [] _lastSaved; // save digest of serialized item, so can tell if updated outside // of setData protected boolean _available = false; // false until first time data is set or updated /** * Track error state in a subclass-compatible way by storing the last exception we threw. */ protected IOException _errorState = null; public NetworkObject() {} // Needed to support serialization of subclasses /** * Subclasses need to specify the type as an argument as well as a template * parameter in order to make factory methods work properly. * @param type Should be same as class template parameter. * @param contentIsMutable Is the class we are encapsulating mutable (its content can * be modified without replacing the object reference) or immutable (the only * way to change it is to replace it, or here set it with setData). Unfortunately * there is no way to determine this via reflection. You could also set this to * false if you do not expose the data directly, but merely expose methods to modify * its values, and manage _isPotentiallyDirty directly. */ public NetworkObject(Class<E> type, boolean contentIsMutable) { _type = type; _contentIsMutable = contentIsMutable; } /** * Specify type as well as initial data. * @param type Should be same as class template parameter. * @param contentIsMutable Is the class we are encapsulating mutable (its content can * be modified without replacing the object reference) or immutable (the only * way to change it is to replace it, or here set it with setData). Unfortunately * there is no way to determine this via reflection. You could also set this to * false if you do not expose the data directly, but merely expose methods to modify * its values, and manage _isPotentiallyDirty directly. * @param data Initial data value. */ public NetworkObject(Class<E> type, boolean contentIsMutable, E data) { this(type, contentIsMutable); setData(data); // marks data as available if non-null } protected NetworkObject(Class<E> type, NetworkObject<? extends E> other) { this(type, other._contentIsMutable); _data = other._data; _isDirty = other._isDirty; _isPotentiallyDirty = other._isPotentiallyDirty; _lastSaved = other._lastSaved; _available = other._available; } /** * Create an instance of the parameterized type, used for decoding. * @return the new instance * @throws IOException wrapping other types of exception generated by constructor. */ protected E factory() throws IOException { E newE; try { newE = _type.newInstance(); } catch (InstantiationException e) { Log.warning("Cannot wrap class " + _type.getName() + " -- impossible to construct instances!"); throw new IOException("Cannot wrap class " + _type.getName() + " -- impossible to construct instances!"); } catch (IllegalAccessException e) { Log.warning("Cannot wrap class " + _type.getName() + " -- cannot access default constructor!"); throw new IOException("Cannot wrap class " + _type.getName() + " -- cannot access default constructor!"); } return newE; } /** * Read this object's data from the network. * @param input InputStream holding the object's data. * @throws ContentDecodingException if there is an error decoding the object * @throws IOException if there is an error reading the object from the network */ public void update(InputStream input) throws ContentDecodingException, IOException { E newData = readObjectImpl(input); synchronized(this) { if (!_available) { if (Log.isLoggable(Log.FAC_IO, Level.FINEST)) { Log.finest(Log.FAC_IO, "Update -- first initialization."); } } _data = newData; _available = true; setDirty(false); _lastSaved = digestContent(); } } /** * @return true if the object has been updated from the network, or has had * its data value set to a non-null value (whether it has been saved to the * network or not). */ public synchronized boolean available() { return _available; } public synchronized boolean hasError() { return (null != _errorState); } public synchronized IOException getError() { return _errorState; } public synchronized void clearError() { _errorState = null; } protected synchronized void setError(IOException t) { _errorState = t; } /** * Set a new data value for this object. Mark it as dirty (needing * to be saved). * @param data new value */ public synchronized void setData(E data) { if (null != _data) { if (!_data.equals(data)) { _data = data; setDirty(true); setAvailable(data != null); } // else -- setting to same value, not dirty, do nothing } else { if (data != null) { _data = data; setDirty(true); setAvailable(true); } // else -- setting from null to null, do nothing } } /** * @param available the new value */ protected synchronized void setAvailable(boolean available) { _available = available; } /** * Retrieve this object's data. * Subclasses should expose methods to access/modify _data, * but may choose not to expose _data itself. Ideally any dangerous operation * (like giving access to some variable that could be changed) will * mark the object as _isPotentiallyDirty. Changes to the data will * then be detected automatically. (The use of _isPotentiallyDirty * to control detection of content change is an optimization, otherwise * isDirty() is invoked every time the object might need to be saved.) * @return Returns the data. Whether null data is allowed or not is * determined by the subclass, which can override available() (by * default, data cannot be null). * @throws ContentNotReadyException if the object has not finished retrieving data/having data set * @throws ErrorStateException */ protected synchronized E data() throws ContentNotReadyException, ContentGoneException, ErrorStateException { if (hasError()) { throw new ErrorStateException("Cannot retrieve data -- object in error state!", _errorState); } if (!available()) { throw new ContentNotReadyException("No data yet saved or retrieved!"); } // Mark that we've given out access to the internal data, so we know someone might // have changed it. If it can't be changed outside this interface, don't // mark it potentially dirty as an optimization. if (_contentIsMutable) _isPotentiallyDirty = true; // return a pointer to the current data. No guarantee that this will continue // to be what we think our data unless caller holds read lock. return _data; } /** * Save the object regardless of whether it has been modified (isDirty()) or not. * @param output stream to save to * @throws ContentEncodingException if there is an error encoding the object * @throws IOException if there is an error writing the object to the network */ public synchronized void forceSave(OutputStream output) throws ContentEncodingException, IOException { if (null == _data) { throw new InvalidObjectException("No data to save!"); } internalWriteObject(output); } /** * Save the object if it is dirty (has been changed). * @param output stream to write to. * @throws ContentEncodingException if there is an error encoding the object * @throws IOException if there is an error writing the object to the network. */ public synchronized void save(OutputStream output) throws ContentEncodingException, IOException { if (available() && isDirty()) { forceSave(output); } } /** * Encode and digest the object's content in order to detect changes made * outside of the object's own interface (for example, if the data is accessed * using data() and then modified). * @return * @throws ContentEncodingException if there is a problem encoding the content * @throws IOException if there is a problem writing the object to the stream. */ protected byte [] digestContent() throws ContentEncodingException, IOException { try { // Otherwise, might have been written when we weren't looking (someone accessed // data and then changed it). DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance(DEFAULT_CHECKSUM_ALGORITHM)); writeObjectImpl(dos); dos.flush(); dos.close(); byte [] currentValue = dos.getMessageDigest().digest(); return currentValue; } catch (NoSuchAlgorithmException e) { Log.warning("No pre-configured algorithm {0} available -- configuration error!", DEFAULT_CHECKSUM_ALGORITHM); throw new RuntimeException("No pre-configured algorithm " + DEFAULT_CHECKSUM_ALGORITHM + " available -- configuration error!"); } } /** * Encode the object and see whether its digest has changed since last time * it was saved. Conservative, only runs the full check if _isPotentiallyDirty * is true. * @return true if the object has been modified. * @throws IOException if there is a problem encoding the object. */ protected synchronized boolean isDirty() throws ContentEncodingException, IOException { if (_isDirty) { return _isDirty; } else if (_lastSaved == null) { if (_data == null) return false; return true; } if (_isPotentiallyDirty) { byte [] currentValue = digestContent(); if (Arrays.equals(currentValue, _lastSaved)) { Log.finest("Last saved value for object still current."); _isDirty = false; } else { Log.finer("Last saved value for object not current -- object changed."); _isDirty = true; } } else { // We've never set the data, nor given out access to it. It can't be dirty. Log.finest("NetworkObject: data cannot be dirty."); _isDirty = false; } return _isDirty; } /** * @return True if the content was either read from the network or was saved locally. */ public synchronized boolean isSaved() throws IOException { return available() && !isDirty(); } /** * @param dirty new value for the dirty setting. */ protected synchronized void setDirty(boolean dirty) { _isDirty = dirty; if (!_isDirty) { _isPotentiallyDirty = false; // just read or written } } /** * Extract the content digest (made with the default digest algorithm). * @throws IOException */ public byte [] getContentDigest() throws IOException { if (!isSaved()) { throw new ErrorStateException("Content has not been saved!"); } return _lastSaved; } /** * Save the object and update the internal tracking digest of its last-saved content. * @param output stream to write to. * @throws ContentEncodingException if there is an error encoding the object * @throws IOException if there is an error writing the object to the network */ protected synchronized void internalWriteObject(OutputStream output) throws ContentEncodingException, IOException { try { DigestOutputStream dos = new DigestOutputStream(output, MessageDigest.getInstance(DEFAULT_CHECKSUM_ALGORITHM)); writeObjectImpl(dos); dos.flush(); // do not close dos, as it will close the output, allow caller to close _lastSaved = dos.getMessageDigest().digest(); setDirty(false); } catch (NoSuchAlgorithmException e) { Log.warning("No pre-configured algorithm {0} available -- configuration error!", DEFAULT_CHECKSUM_ALGORITHM); throw new RuntimeException("No pre-configured algorithm " + DEFAULT_CHECKSUM_ALGORITHM + " available -- configuration error!"); } } /** * Subclasses override. This implements the actual object write. No flush or close necessary. * @param output the stream to write to * @throws ContentEncodingException if there is an error encoding the object * @throws IOException if there is an error writing it to the network */ protected abstract void writeObjectImpl(OutputStream output) throws ContentEncodingException, IOException; /** * Subclasses override. This implements the actual object read from stream, returning * the new object. * @throws ContentDecodingException if there is an error decoding the object * @throws IOException if there is an error actually reading the data */ protected abstract E readObjectImpl(InputStream input) throws ContentDecodingException, IOException; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((_data == null) ? 0 : _data.hashCode()); result = prime * result + ((_type == null) ? 0 : _type.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; NetworkObject<?> other = (NetworkObject<?>) obj; if (_type == null) { if (other._type != null) return false; } else if (!_type.equals(other._type)) return false; if (_data == null) { if (other._data != null) return false; } else if (!_data.equals(other._data)) return false; return true; } /** * Equality comparison on just the internal data. * @param obj * @return true if other is a NetworkObject and the two have matching data(). */ public boolean contentEquals(Object obj) { if (getClass() != obj.getClass()) return false; NetworkObject<?> other = (NetworkObject<?>) obj; if (_data == null) { if (other._data != null) { return false; } else { return true; } } return _data.equals(other._data); } @Override public String toString() { return (null == _data) ? "(null)" : _data.toString(); } }