/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.isis.core.runtime.persistence.adapter;
import org.datanucleus.enhancement.Persistable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.isis.core.commons.authentication.AuthenticationSession;
import org.apache.isis.core.commons.ensure.Ensure;
import org.apache.isis.core.commons.exceptions.IsisException;
import org.apache.isis.core.commons.util.ToString;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
import org.apache.isis.core.metamodel.adapter.oid.Oid;
import org.apache.isis.core.metamodel.adapter.oid.ParentedCollectionOid;
import org.apache.isis.core.metamodel.adapter.oid.RootOid;
import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
import org.apache.isis.core.metamodel.adapter.version.Version;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
import org.apache.isis.core.metamodel.spec.ElementSpecificationProvider;
import org.apache.isis.core.metamodel.spec.Instance;
import org.apache.isis.core.metamodel.spec.InstanceAbstract;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.Specification;
import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
import org.apache.isis.core.runtime.system.persistence.PersistenceSession;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
public class PojoAdapter extends InstanceAbstract implements ObjectAdapter {
private final static Logger LOG = LoggerFactory.getLogger(PojoAdapter.class);
//region > Constructor, fields, finalizer
private final AuthenticationSession authenticationSession;
private final SpecificationLoader specificationLoader;
private final PersistenceSession persistenceSession;
/**
* can be {@link #replacePojo(Object) replace}d.
*/
private Object pojo;
/**
* can be {@link #replaceOid(Oid) replace}d.
*/
private Oid oid;
/**
* only for standalone or parented collections.
*/
private ElementSpecificationProvider elementSpecificationProvider;
public PojoAdapter(
final Object pojo,
final Oid oid,
final AuthenticationSession authenticationSession,
final SpecificationLoader specificationLoader,
final PersistenceSession persistenceSession) {
this.persistenceSession = persistenceSession;
this.specificationLoader = specificationLoader;
this.authenticationSession = authenticationSession;
if (pojo instanceof ObjectAdapter) {
throw new IsisException("Adapter can't be used to adapt an adapter: " + pojo);
}
this.pojo = pojo;
this.oid = oid;
}
//endregion
//region > getSpecification
/**
* Downcasts {@link #getSpecification()}.
*/
@Override
public ObjectSpecification getSpecification() {
return (ObjectSpecification) super.getSpecification();
}
@Override
protected ObjectSpecification loadSpecification() {
final Class<?> aClass = getObject().getClass();
final ObjectSpecification specification = specificationLoader.loadSpecification(aClass);
return specification;
}
//endregion
//region > getObject, replacePojo
@Override
public Object getObject() {
return pojo;
}
/**
* Sometimes it is necessary to manage the replacement of the underlying
* domain object (by another component such as an object store). This method
* allows the adapter to be kept while the domain object is replaced.
*/
@Override
public void replacePojo(final Object pojo) {
this.pojo = pojo;
}
//endregion
//region > getOid, replaceOid
@Override
public Oid getOid() {
return oid;
}
@Override
public void replaceOid(Oid persistedOid) {
Ensure.ensureThatArg(oid, is(notNullValue())); // values have no oid, so cannot be replaced
this.oid = persistedOid;
}
//endregion
//region > isParentedCollection, isValue
@Override
public boolean isParentedCollection() {
return oid instanceof ParentedCollectionOid;
}
@Override
public boolean isValue() {
return oid == null;
}
//endregion
//region > isTransient, representsPersistent, isDestroyed
@Override
public boolean isTransient() {
if(getSpecification().isService() || getSpecification().isViewModel()) {
// services and view models are treated as persistent objects
return false;
}
if (pojo instanceof Persistable) {
final Persistable pojo = (Persistable) this.pojo;
final boolean isPersistent = pojo.dnIsPersistent();
final boolean isDeleted = pojo.dnIsDeleted();
if (!isPersistent && !isDeleted) {
return true;
}
}
return false;
}
@Override
public boolean representsPersistent() {
if(getSpecification().isService() || getSpecification().isViewModel()) {
// services and view models are treated as persistent objects
return true;
}
if (pojo instanceof Persistable) {
final Persistable pojo = (Persistable) this.pojo;
final boolean isPersistent = pojo.dnIsPersistent();
final boolean isDeleted = pojo.dnIsDeleted();
// REVIEW: should we also ensure !isDeleted ???
if (isPersistent) {
return true;
}
}
return false;
}
@Override
public boolean isDestroyed() {
if(getSpecification().isService() || getSpecification().isViewModel()) {
// services and view models are treated as persistent objects
return false;
}
if (pojo instanceof Persistable) {
final Persistable pojo = (Persistable) this.pojo;
final boolean isDeleted = pojo.dnIsDeleted();
if (isDeleted) {
return true;
}
}
return false;
}
//endregion
//region > getAggregateRoot
@Override
public ObjectAdapter getAggregateRoot() {
if(!isParentedCollection()) {
return this;
}
ParentedCollectionOid collectionOid = (ParentedCollectionOid) oid;
return persistenceSession.getAggregateRoot(collectionOid);
}
//endregion
//region > getVersion, setVersion, checkLock
@Override
public Version getVersion() {
if(isParentedCollection()) {
return getAggregateRoot().getVersion();
} else {
return getOid().getVersion();
}
}
@Override
public void checkLock(final Version otherVersion) {
if(isParentedCollection()) {
getAggregateRoot().checkLock(otherVersion);
return;
}
Oid thisOid = getOid();
final Version thisVersion = thisOid.getVersion();
// check for exception, but don't throw if suppressed through thread-local
if(thisVersion != null &&
otherVersion != null &&
thisVersion.different(otherVersion)) {
if(AdapterManager.ConcurrencyChecking.isCurrentlyEnabled()) {
LOG.info("concurrency conflict detected on " + thisOid + " (" + otherVersion + ")");
final String currentUser = authenticationSession.getUserName();
throw new ConcurrencyException(currentUser, thisOid, thisVersion, otherVersion);
} else {
LOG.info("concurrency conflict detected but suppressed, on " + thisOid + " (" + otherVersion + ")");
}
}
}
@Override
public void setVersion(final Version version) {
if(isParentedCollection()) {
// ignored
return;
}
if (shouldSetVersion(version)) {
RootOid rootOid = (RootOid) getOid(); // since not parented
rootOid.setVersion(version);
}
}
private boolean shouldSetVersion(final Version otherVersion) {
final Version version = getOid().getVersion();
return version == null || otherVersion == null || otherVersion.different(version);
}
//endregion
//region > titleString
/**
* Returns the title from the underlying business object.
*
* <p>
* If the object has not yet been resolved the specification will be asked
* for a unresolved title, which could of been persisted by the persistence
* mechanism. If either of the above provides null as the title then this
* method will return a title relating to the name of the object type, e.g.
* "A Customer", "A Product".
*/
@Override
public String titleString() {
return titleString(null);
}
@Override
public String titleString(ObjectAdapter contextAdapterIfAny) {
if (getSpecification().isParentedOrFreeCollection()) {
final CollectionFacet facet = getSpecification().getFacet(CollectionFacet.class);
return collectionTitleString(facet);
} else {
return objectTitleString(contextAdapterIfAny);
}
}
private String objectTitleString(ObjectAdapter contextAdapterIfAny) {
if (getObject() instanceof String) {
return (String) getObject();
}
final ObjectSpecification specification = getSpecification();
String title = specification.getTitle(contextAdapterIfAny, this);
if (title == null) {
title = getDefaultTitle();
}
return title;
}
private String collectionTitleString(final CollectionFacet facet) {
final int size = facet.size(this);
final ObjectSpecification elementSpecification = getElementSpecification();
if (elementSpecification == null || elementSpecification.getFullIdentifier().equals(Object.class.getName())) {
switch (size) {
case -1:
return "Objects";
case 0:
return "No objects";
case 1:
return "1 object";
default:
return size + " objects";
}
} else {
switch (size) {
case -1:
return elementSpecification.getPluralName();
case 0:
return "No " + elementSpecification.getPluralName();
case 1:
return "1 " + elementSpecification.getSingularName();
default:
return size + " " + elementSpecification.getPluralName();
}
}
}
@Override
public String toString() {
final ToString str = new ToString(this);
toString(str);
// don't do title of any entities. For persistence entities, might
// forces an unwanted resolve
// of the object. For transient objects, may not be fully initialized.
str.append("pojo-toString", pojo.toString());
str.appendAsHex("pojo-hash", pojo.hashCode());
return str.toString();
}
protected String getDefaultTitle() {
return "A" + (" " + getSpecification().getSingularName()).toLowerCase();
}
protected void toString(final ToString str) {
str.append(aggregateResolveStateCode());
final Oid oid = getOid();
if (oid != null) {
str.append(":");
str.append(oid.toString());
} else {
str.append(":-");
}
str.setAddComma();
if (getSpecificationNoLoad() == null) {
str.append("class", getObject().getClass().getName());
} else {
str.append("specification", getSpecification().getShortIdentifier());
}
if(getOid() != null) {
final Version version = getOid().getVersion();
str.append("version", version != null ? version.sequence() : null);
}
}
private String aggregateResolveStateCode() {
// this is an approximate re-implementation...
final Oid oid = getOid();
if(oid != null) {
if(oid.isPersistent()) return "P";
if(oid.isTransient()) return "T";
if(oid.isViewModel()) return "V";
}
return "S"; // standalone adapter (value)
}
//endregion
//region > iconName
/**
* Returns the name of the icon to use to represent this object.
*/
@Override
public String getIconName() {
return getSpecification().getIconName(this);
}
//endregion
//region > elementSpecification
@Override
public ObjectSpecification getElementSpecification() {
if (elementSpecificationProvider == null) {
return null;
}
return elementSpecificationProvider.getElementType();
}
/**
* Called whenever there is a {@link org.apache.isis.core.metamodel.facets.actcoll.typeof.TypeOfFacet} present.
*
* <p>
* Specifically, if an action which has been annotated (is copied by {@link org.apache.isis.core.metamodel.facets.actions.action.invocation.ActionInvocationFacet action invocation facet}), and for a parented collection
* (is copied by the {@link PersistenceSession} when {@link PersistenceSession#adapterFor(Object, ObjectAdapter, OneToManyAssociation) creating} an adapter for a collection.
* </p>
*/
@Override
public void setElementSpecificationProvider(final ElementSpecificationProvider elementSpecificationProvider) {
this.elementSpecificationProvider = elementSpecificationProvider;
}
//endregion
//region > getInstance (unsupported for this impl)
/**
* Not supported by this implementation.
*/
@Override
public Instance getInstance(final Specification specification) {
throw new UnsupportedOperationException();
}
//endregion
}