/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
package org.eclipse.ecr.core.storage.sql;
import java.io.Serializable;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import org.eclipse.ecr.core.storage.StorageException;
/**
* A rich value corresponding to one row or a collection of rows in a table.
* <p>
* In addition to the basic {@link Row}, this holds the old values (to check
* dirty state), the state and a reference to the session.
* <p>
* This class has two kinds of state-changing methods:
* <ul>
* <li>the "set" ones, which only change the state,</li>
* <li>the "mark" ones, which change the state and do the corresponding changes
* in the pristine/modified maps of the context.</li>
* <li></li>
* </ul>
*
* @author Florent Guillaume
*/
public abstract class Fragment implements Serializable {
private static final long serialVersionUID = 1L;
/**
* The possible states of a fragment.
*/
public enum State {
/**
* The fragment is not attached to a persistence context.
*/
DETACHED, // first is default
/**
* The fragment has been read and found to be absent in the database. It
* contains default data (usually {@code null}). It lives in the
* context's pristine map. Upon modification, the state will change to
* {@link #CREATED}.
*/
ABSENT,
/**
* The fragment exists in the database but hasn't been changed yet. It
* lives in the context's pristine map. Upon modification, the state
* will change to {@link #MODIFIED}.
*/
PRISTINE,
/**
* The fragment does not exist in the database and will be inserted upon
* save. It lives in the context's modified map. Upon save it will be
* inserted in the database and the state will change to
* {@link #PRISTINE}.
*/
CREATED,
/**
* The fragment has been modified. It lives in the context's modified
* map. Upon save the database will be updated and the state will change
* to {@link #CREATED}.
*/
MODIFIED,
/**
* The fragment has been deleted. It lives in the context's modified
* map. Upon save it will be deleted from the database and the state
* will change to {@link #DETACHED}.
*/
DELETED,
/**
* The fragment has been invalidated by a modification or creation. Any
* access must refetch it. It lives in the context's pristine map.
*/
INVALIDATED_MODIFIED,
/**
* The fragment has been invalidated by a deletion. It lives in the
* context's pristine map.
*/
INVALIDATED_DELETED
}
/**
* The row holding the data.
*/
protected Row row;
/**
* The row old values, from the time of construction / refetch. The size of
* the the array is following {@link #row.values.length}.
*/
protected Serializable[] oldvalues;
private State state; // default is DETACHED
protected PersistenceContext context;
/**
* Constructs a {@link Fragment} from a {@link Row}.
*
* @param row the row
* @param state the initial state for the fragment
* @param context the persistence context to which the fragment is tied, or
* {@code null}
*/
protected Fragment(Row row, State state, PersistenceContext context) {
this.row = row;
this.state = state;
this.context = context;
switch (state) {
case DETACHED:
if (context != null) {
throw new IllegalArgumentException();
}
break;
case CREATED:
case DELETED:
context.setFragmentModified(this); // not in pristine
break;
case ABSENT:
case PRISTINE:
context.setFragmentPristine(this); // not in modified
break;
case MODIFIED:
case INVALIDATED_MODIFIED:
case INVALIDATED_DELETED:
throw new IllegalArgumentException(state.toString());
}
clearDirty();
}
/**
* Gets the state.
*
* @return the state
*/
public State getState() {
return state;
}
/**
* Sets the id. This only used at most once to change a temporary id to the
* persistent one.
*
* @param id the new persistent id
*/
public void setId(Serializable id) {
row.id = id;
}
/**
* Gets the id.
*
* @return the id
*/
public Serializable getId() {
return row.id;
}
/**
* Clears the dirty state.
*/
public void clearDirty() {
oldvalues = row.values.clone();
}
/**
* Refetches this fragment from the database. Needed when an invalidation
* has been received and the fragment is accessed again.
*
* @return the new state, {@link State#PRISTINE} or {@link State#ABSENT}
* @throws StorageException
*/
protected abstract State refetch() throws StorageException;
/**
* Resets the data for a fragment that was invalidated by deletion.
*
* @return the new state, {@link State#PRISTINE} or {@link State#ABSENT}
* @throws StorageException
*/
protected abstract State refetchDeleted() throws StorageException;
/**
* Checks that access to the fragment is possible. Called internally before
* a get, so that invalidated fragments can be refetched.
*
* @throws StorageException
*/
protected void accessed() throws StorageException {
switch (state) {
case DETACHED:
case ABSENT:
case PRISTINE:
case CREATED:
case MODIFIED:
case DELETED:
break;
case INVALIDATED_MODIFIED:
state = refetch();
break;
case INVALIDATED_DELETED:
state = refetchDeleted();
}
}
/**
* Marks the fragment modified. Called internally after a put/set.
*/
protected void markModified() {
switch (state) {
case ABSENT:
context.setFragmentModified(this);
state = State.CREATED;
break;
case INVALIDATED_MODIFIED:
// can only happen if overwrite all invalidated (array)
// fall through
case PRISTINE:
context.setFragmentModified(this);
state = State.MODIFIED;
break;
case DETACHED:
case CREATED:
case MODIFIED:
case DELETED:
break;
case INVALIDATED_DELETED:
throw new ConcurrentModificationException(
"Modifying a concurrently deleted value");
}
}
/**
* Marks the fragment deleted. Called after a remove.
*/
protected void setDeleted() {
switch (state) {
case DETACHED:
break;
case ABSENT:
case INVALIDATED_DELETED:
context = null;
state = State.DETACHED;
break;
case CREATED:
context = null;
state = State.DETACHED;
break;
case PRISTINE:
case INVALIDATED_MODIFIED:
state = State.DELETED;
break;
case MODIFIED:
state = State.DELETED;
break;
case DELETED:
throw new RuntimeException(this.toString());
}
}
/**
* Detaches the fragment from its persistence context. The caller makes sure
* that the fragment is removed from the context map.
*/
protected void setDetached() {
state = State.DETACHED;
context = null;
}
/**
* Sets the (created/modified) fragment in the pristine state. Called after
* a save.
*/
protected void setPristine() {
switch (state) {
case CREATED:
case MODIFIED:
state = State.PRISTINE;
break;
case ABSENT:
case PRISTINE:
case DELETED:
case DETACHED:
case INVALIDATED_MODIFIED:
case INVALIDATED_DELETED:
// incoherent with the pristine map + expected state
throw new RuntimeException(this.toString());
}
}
/**
* Sets the fragment in the "invalidated from a modification" state. This is
* called:
* <ul>
* <li>when a database operation does non-tracked changes, which means that
* on access a refetch will be needed,
* <li>during post-commit invalidation.
* </ul>
*/
protected void setInvalidatedModified() {
switch (state) {
case ABSENT:
case PRISTINE:
case CREATED:
case MODIFIED:
case DELETED:
state = State.INVALIDATED_MODIFIED;
break;
case INVALIDATED_MODIFIED:
case INVALIDATED_DELETED:
break;
case DETACHED:
throw new RuntimeException(this.toString());
}
}
/**
* Sets the fragment in the "invalidated from a deletion" state. This is
* called:
* <ul>
* <li>when a database operation does a delete,
* <li>during post-commit invalidation.
* </ul>
*/
protected void setInvalidatedDeleted() {
switch (state) {
case ABSENT:
case PRISTINE:
case CREATED:
case MODIFIED:
case DELETED:
case INVALIDATED_MODIFIED:
state = State.INVALIDATED_DELETED;
break;
case INVALIDATED_DELETED:
break;
case DETACHED:
throw new RuntimeException(this.toString());
}
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(getClass().getSimpleName());
buf.append("(row=");
buf.append(row);
buf.append(", state=");
buf.append(getState());
buf.append(')');
return buf.toString();
}
}
/**
* A fragments map holds all {@link Fragment}s for non-main tables.
*/
class FragmentsMap extends HashMap<String, Fragment> {
private static final long serialVersionUID = 1L;
}
/**
* Utility class grouping a main {@link Fragment} with a related hierarchy
* {@link Fragment} and additional fragments.
* <p>
* If the main and hierarchy tables are not separate, then the hierarchy
* fragment is unused.
* <p>
* This is all the data needed to describe a {@link Node}.
*/
class FragmentGroup {
public final SimpleFragment hier;
public final FragmentsMap fragments;
public FragmentGroup(SimpleFragment hier, FragmentsMap fragments) {
this.hier = hier;
this.fragments = fragments;
}
@Override
public String toString() {
return getClass().getSimpleName() + '(' + hier + ", " + fragments + ')';
}
}