All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.pdo.DefaultDomainContext Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.tentackle.pdo;

import org.tentackle.common.StringHelper;
import org.tentackle.common.TentackleRuntimeException;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.Session;
import org.tentackle.session.SessionInfo;

import java.io.ObjectStreamException;
import java.io.Serial;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;

/**
 * The default application's domain context.
 *
 * @author harald
 */
public class DefaultDomainContext implements DomainContext {

  @Serial
  private static final long serialVersionUID = 1L;

  /** the unnamed context name chain. */
  private static final List UNNAMED_LIST = List.of("");


  // attributes are transient because session could be remote (is serializable)

  /**
   * the session.
   */
  private transient Session session;
  private transient boolean sessionValid;     // false if not created by constructor, i.e. sent over wire

  /**
   * session immutable flag.
   */
  private transient boolean sessionImmutable;

  /**
   * the root entity, null if none.
   * 

* Notice that the root-entity is transient! * For remote sessions the rootId and rootClassId will be transferred. */ private transient PersistentDomainObject rootEntity; /** * Lazily created chained context name.
* Defaults to {@code ""}. */ private transient String chainedName; // non transient /** * ID of the root entity.
* Necessary in remote sessions. */ private long rootId; /** * Class-ID of the root entity.
* Necessary in remote sessions. */ private int rootClassId; /** * thread-local session clone of this domain context. */ private transient DefaultDomainContext tlsContext; /** * the non-root context. */ private DefaultDomainContext nonRootContext; /** * Reference to the cloned context. */ private transient WeakReference clonedContextRef; /** * The context name chain.
* Array is faster to serialize/deserialize than List. */ private String[] names; // null if unnamed (internal only) private transient List namesList; // lazy unmodifiable list for external API /** * Creates a default context. * * @param session the session, null if thread-local * @param sessionImmutable true if session cannot be changed anymore */ public DefaultDomainContext(Session session, boolean sessionImmutable) { this.session = session; // don't use setSession() as this will also assertPermissions this.sessionValid = true; this.sessionImmutable = sessionImmutable; if (session == null) { Session.assertCurrentSessionValid(); } } /** * Creates a mutable default context. * * @param session the session, null if thread-local */ public DefaultDomainContext(Session session) { this(session, false); } @Override public List getNames() { if (namesList == null) { namesList = names == null ? UNNAMED_LIST : List.of(names); } return namesList; } @Override public boolean isSessionThreadLocal() { return session == null; } /** * {@inheritDoc}
* If the context's session is null the thread's local * session is returned. * * @see Session#getCurrentSession() */ @Override public Session getSession() { if (isSessionThreadLocal()) { return Session.getCurrentSession(); } return session; } @Override public void setSession(Session session) { if (sessionValid) { if (isSessionThreadLocal()) { if (session != null && session != getSession()) { // A thread-local session cannot be changed to another fixed session. // To do so, a new context is necessary. // Same session as thread-local is ok, however. throw new PersistenceException("illegal attempt to change the thread-local session of " + getClass().getSimpleName() + " '" + this + "' from " + getSession() + " to " + session); } // else no change: requested session is null or the same as the thread-local session } else { // this.session != null if (isSessionImmutable() && this.session != session) { throw new PersistenceException(this.session, "illegal attempt to change the immutable session of " + getClass().getSimpleName() + " '" + this + "' from " + this.session + " to " + session); } this.session = session; if (nonRootContext != null) { nonRootContext.session = session; } } } else { // first setting after deserialization (DomainContext was not created by constructor!) this.session = session; if (session == null) { Session.assertCurrentSessionValid(); } sessionValid = true; assertPermissions(); } } /** * Sets the session to immutable. * * @param sessionImmutable true if session cannot be changed anymore */ @Override public void setSessionImmutable(boolean sessionImmutable) { this.sessionImmutable = sessionImmutable; } /** * Returns whether the session is immutable. * * @return true if immutable */ @Override public boolean isSessionImmutable() { return sessionImmutable; } @Override public int getSessionInstanceNumber() { // the instance-no must be 0 for dynamic thread-local session (due to sorting, e.g. in caches) return session == null ? 0 : session.getInstanceNumber(); } @Override public SessionInfo getSessionInfo() { Session s = getSession(); if (s == null) { throw new PdoRuntimeException("no session for context " + this); } return s.getSessionInfo(); } @Override public void assertPermissions() { } @Override public PersistentDomainObject getContextPdo() { return null; } @Override public long getContextId() { return 0; } @Override public boolean isWithinContext(long contextId, int contextClassId) { return contextId < 0 || contextId == getContextId(); } @Override public boolean isWithinContext(String name) { for (String n : getNames()) { // getNames will always begin with the empty string! if (n.equals(name)) { return true; } } return false; } /** * Gets the string representation of this context. * The default implementation returns the string of the context object, * or the empty string (not the null-string!), if no such object, * which is the case for the plain context. * * @return the string */ @Override public String toString() { // context does not include Session.toString() because DomainContext.toString() is // heavily used in GUIs to show the user's context. return getContextId() == 0 ? "" : getContextPdo().toString(); } @Override public String toGenericString() { if (chainedName == null) { chainedName = createChainedName(); } long contextId = getContextId(); if (contextId != 0) { return chainedName + "[" + getContextId() + "]"; } return chainedName; } @Override public String toDiagnosticString() { StringBuilder buf = new StringBuilder(); if (isRootContext()) { buf.append("root "); } buf.append("context '").append(toGenericString()).append("' using "); if (isSessionThreadLocal()) { buf.append("thread-local "); } // thread-local implicitly means immutable else if (isSessionImmutable()) { buf.append("immutable "); } buf.append("session "); Session session = getSession(); if (session != null) { buf.append(session.getName()); } else { buf.append(""); } return buf.toString(); } /** * Compares this domain context with another domain context.
* The default implementation just compares the class, the contextId * and the session.
* Checking against the null context returns 1. * * @param otherContext the context to compare this context to */ @Override public int compareTo(DomainContext otherContext) { if (otherContext == null) { return 1; } int rv = getClass().hashCode() - otherContext.getClass().hashCode(); if (rv == 0) { rv = Long.compare(getContextId(), otherContext.getContextId()); if (rv == 0) { rv = getSessionInstanceNumber() - otherContext.getSessionInstanceNumber(); } } return rv; } /** * {@inheritDoc} *

* Overridden to check whether contexts are equal.
* The default implementation checks the class, the contextId and session for equality. * Checking against the null context returns false. */ @Override public boolean equals(Object obj) { return obj != null && getClass() == obj.getClass() && getContextId() == ((DomainContext) obj).getContextId() && getSessionInstanceNumber() == ((DomainContext) obj).getSessionInstanceNumber(); } @Override public int hashCode() { int hash = 7 + (int) getContextId() + (getClass().hashCode() & 0xffff); hash = 53 * hash + (session != null ? session.hashCode() : 0); // not getSession() bec. of thread-local! return hash; } @Override public DefaultDomainContext cloneKeepRoot() { try { DefaultDomainContext context = (DefaultDomainContext) super.clone(); context.sessionImmutable = false; context.tlsContext = null; context.clonedContextRef = new WeakReference<>(this); return context; } catch (CloneNotSupportedException ex) { throw new TentackleRuntimeException(ex); // this shouldn't happen, since we are Cloneable } } @Override public DefaultDomainContext clone() { DefaultDomainContext context = cloneKeepRoot(); context.clearRoot(); return context; } @Override public DefaultDomainContext cloneKeepRoot(String name) { if (StringHelper.isAllWhitespace(name) || name.contains(":")) { throw new DomainException("illegal context name: '" + name + "'"); } if (isWithinContext(name)) { throw new DomainException("context name '" + name + "' already within " + toDiagnosticString()); } DefaultDomainContext context = cloneKeepRoot(); context.namesList = null; if (context.names == null) { context.names = new String[]{"", name}; } else { int oldLen = context.names.length; context.names = Arrays.copyOf(context.names, oldLen + 1); context.names[oldLen] = name; } return context; } @Override public DefaultDomainContext clone(String name) { DefaultDomainContext context = cloneKeepRoot(name); context.clearRoot(); return context; } @Override public DomainContext getClonedContext() { DomainContext clonedContext = null; if (clonedContextRef != null) { clonedContext = clonedContextRef.get(); if (clonedContext == null) { clonedContextRef = null; // GC'd } } return clonedContext; } @Override public DomainContext getThreadLocalSessionContext() { if (tlsContext == null) { tlsContext = clone(); tlsContext.session = null; tlsContext.sessionImmutable = true; tlsContext.tlsContext = tlsContext; } return tlsContext; } @Override public void clearThreadLocalSessionContext() { tlsContext = null; } @Override public DefaultDomainContext getNonRootContext() { DefaultDomainContext context = nonRootContext != null ? nonRootContext : this; if (context.isRootContext()) { throw new PersistenceException(context + " is a root context"); } return context; } @Override public boolean isRootContext() { return getRootClassId() != 0; } @Override public DomainContext getRootContext(PersistentDomainObject rootEntity) { if (rootEntity == null) { throw new PersistenceException("rootEntity must not be null"); } if (this.rootEntity == rootEntity) { return this; } /* * Important: * we cannot invoke methods on rootEntity if we're in PDO creation because the delegate * being created is not yet available by the proxy's mixin. * Hence, we load the rootClassId and rootId when invoked the first time * or when serialized (see writeReplace()). */ PersistentObject po = rootEntity.getPersistenceDelegate(); if (po != null) { // not in object creation: we can invoke methods if (!po.isRootEntity()) { throw new PersistenceException(rootEntity.toGenericString() + " is not a root entity"); } if (po.getId() == rootId && po.getRootClassId() == rootClassId) { // just update the link this.rootEntity = rootEntity; return this; } } // create a new root context DefaultDomainContext rootContext = clone(); rootContext.rootEntity = rootEntity; rootContext.nonRootContext = getNonRootContext(); return rootContext; } /** * This does the trick to set up the non-transient rootId and rootClassId when sent via rmi the first time. * * @return me * @throws ObjectStreamException to fulfill the signature only * @see ObjectStreamException */ @Serial // public! Otherwise, writeReplace would not be invoked in subclasses! public Object writeReplace() throws ObjectStreamException { if (rootEntity != null && rootClassId == 0) { rootClassId = rootEntity.getPersistenceDelegate().getClassId(); rootId = rootEntity.getPersistenceDelegate().getId(); } return this; } @Override public PersistentDomainObject getRootEntity() { return rootEntity; } @Override public int getRootClassId() { return rootEntity == null ? rootClassId : rootEntity.getPersistenceDelegate().getClassId(); } @Override public long getRootId() { return rootEntity == null ? rootId : rootEntity.getPersistenceDelegate().getId(); } private void clearRoot() { rootEntity = null; rootId = 0; rootClassId = 0; } private String createChainedName() { if (names == null) { return ""; } StringBuilder buf = new StringBuilder(); boolean needColon = false; for (String name : names) { if (!name.isEmpty()) { // skip first "" if (needColon) { buf.append(':'); } else { needColon = true; } buf.append(name); } } return buf.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy