/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage.sql;
import java.io.Serializable;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import org.nuxeo.ecm.core.api.model.Delta;
import org.nuxeo.ecm.core.storage.sql.RowMapper.RowUpdate;
/**
* 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 #PRISTINE}.
*/
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 deleted as a consequence of another fragment being deleted (cascade). It lives in the
* context's modified map. Upon save it will be implicitly deleted from the database by the deletion of a
* {@link #DELETED} fragment, and the state will change to {@link #DETACHED}.
*/
DELETED_DEPENDENT,
/**
* 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:
case DELETED_DEPENDENT:
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() {
// turn back deltas into full values
Serializable[] values = row.values;
int len = values.length;
for (int i = 0; i < len; i++) {
Serializable ob = values[i];
if (ob instanceof Delta) {
values[i] = ((Delta) ob).getFullValue();
}
}
// clone to clear the dirty state
oldvalues = values.clone();
}
/**
* Returns the row update to do in the database to write this value.
*
* @return a row update, or {@code null} if the value is unchanged since last clear
* @since 8.3
*/
public abstract RowUpdate getRowUpdate();
/**
* 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}
*/
protected abstract State refetch();
/**
* Resets the data for a fragment that was invalidated by deletion.
*
* @return the new state, {@link State#PRISTINE} or {@link State#ABSENT}
*/
protected abstract State refetchDeleted();
/**
* Checks that access to the fragment is possible. Called internally before a get, so that invalidated fragments can
* be refetched.
*/
protected void accessed() {
switch (state) {
case DETACHED:
case ABSENT:
case PRISTINE:
case CREATED:
case MODIFIED:
case DELETED:
case DELETED_DEPENDENT:
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:
case DELETED_DEPENDENT:
break;
case INVALIDATED_DELETED:
throw new ConcurrentModificationException("Modifying a concurrently deleted value");
}
}
/**
* Marks the fragment deleted. Called after a remove.
*/
protected void setDeleted(boolean primary) {
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 = primary ? State.DELETED : State.DELETED_DEPENDENT;
break;
case MODIFIED:
state = primary ? State.DELETED : State.DELETED_DEPENDENT;
break;
case DELETED:
case DELETED_DEPENDENT:
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 DELETED_DEPENDENT:
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:
case DELETED_DEPENDENT:
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 DELETED_DEPENDENT:
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 + ')';
}
}