blob: 9fe63bc6be6ef22e75f3f963a32b9e5c9a665ff8 [file] [log] [blame]
/*
* 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.geode.modules.session.catalina;
import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpSession;
import org.apache.catalina.Manager;
import org.apache.catalina.ha.session.SerializablePrincipal;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.security.SecurityUtil;
import org.apache.catalina.session.StandardSession;
import org.apache.juli.logging.Log;
import org.apache.geode.DataSerializable;
import org.apache.geode.DataSerializer;
import org.apache.geode.Delta;
import org.apache.geode.InvalidDeltaException;
import org.apache.geode.cache.Region;
import org.apache.geode.internal.size.Sizeable;
import org.apache.geode.internal.util.BlobHelper;
import org.apache.geode.modules.gatewaydelta.GatewayDelta;
import org.apache.geode.modules.gatewaydelta.GatewayDeltaEvent;
import org.apache.geode.modules.session.catalina.internal.DeltaSessionAttributeEvent;
import org.apache.geode.modules.session.catalina.internal.DeltaSessionAttributeEventBatch;
import org.apache.geode.modules.session.catalina.internal.DeltaSessionDestroyAttributeEvent;
import org.apache.geode.modules.session.catalina.internal.DeltaSessionUpdateAttributeEvent;
@SuppressWarnings("serial")
public class DeltaSession extends StandardSession
implements DataSerializable, Delta, GatewayDelta, Sizeable, DeltaSessionInterface {
private transient Region<String, DeltaSessionInterface> operatingRegion;
private String sessionRegionName;
private String contextName;
private boolean hasDelta;
private boolean applyRemotely;
private boolean enableGatewayDeltaReplication;
private final transient Object changeLock = new Object();
private final ArrayList<DeltaSessionAttributeEvent> eventQueue = new ArrayList<>();
private transient GatewayDeltaEvent currentGatewayDeltaEvent;
private transient boolean expired = false;
/**
* @deprecated No replacement. Always prefer deserialized form.
*/
@Deprecated
private transient boolean preferDeserializedForm = true;
private byte[] serializedPrincipal;
private static final Field cachedField;
static {
try {
cachedField = StandardSession.class.getDeclaredField("attributes");
cachedField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new IllegalStateException(e);
}
}
/**
* Construct a new <code>Session</code> associated with no <code>Manager</code>. The
* <code>Manager</code> will be assigned later using {@link #setOwner(Object)}.
*/
public DeltaSession() {
super(null);
}
/**
* Construct a new Session associated with the specified Manager.
*
* @param manager The manager with which this Session is associated
*/
public DeltaSession(Manager manager) {
super(manager);
setOwner(manager);
}
/**
* Return the <code>HttpSession</code> for which this object is the facade.
*/
@Override
public HttpSession getSession() {
if (facade == null) {
if (isPackageProtectionEnabled()) {
final DeltaSession fsession = this;
facade = getNewFacade(fsession);
} else {
facade = new DeltaSessionFacade(this);
}
}
return (facade);
}
@Override
public Principal getPrincipal() {
final DeltaSessionManager<?> deltaSessionManager = getDeltaSessionManager();
if (principal == null && serializedPrincipal != null) {
final Log logger = deltaSessionManager.getLogger();
final SerializablePrincipal sp;
try {
sp = (SerializablePrincipal) BlobHelper.deserializeBlob(serializedPrincipal);
} catch (Exception e) {
logger.warn(this
+ ": Serialized principal contains a byte[] that cannot be deserialized due to the following exception",
e);
return null;
}
principal = sp.getPrincipal(deltaSessionManager.getTheContext().getRealm());
if (logger.isDebugEnabled()) {
logger.debug(this + ": Deserialized principal: " + principal);
}
}
return principal;
}
@Override
public void setPrincipal(Principal principal) {
super.setPrincipal(principal);
// Put the session into the region to serialize the principal
if (getManager() != null) {
// TODO convert this to a delta
getManager().add(this);
final Log logger = getDeltaSessionManager().getLogger();
if (logger.isDebugEnabled()) {
logger.debug(this + ": Cached principal: " + principal);
}
}
}
private byte[] getSerializedPrincipal() {
if (serializedPrincipal == null) {
if (principal != null && principal instanceof GenericPrincipal) {
GenericPrincipal gp = (GenericPrincipal) principal;
SerializablePrincipal sp = SerializablePrincipal.createPrincipal(gp);
serializedPrincipal = serialize(sp);
if (manager != null) {
final Log logger = getDeltaSessionManager().getLogger();
if (logger.isDebugEnabled()) {
logger.debug(this + ": Serialized principal: " + sp);
}
}
}
}
return serializedPrincipal;
}
private Region<String, DeltaSessionInterface> getOperatingRegion() {
// This region shouldn't be null when it is needed.
// It should have been set by the setOwner method.
return operatingRegion;
}
boolean isCommitEnabled() {
return getDeltaSessionManager().isCommitValveEnabled();
}
@Override
public GatewayDeltaEvent getCurrentGatewayDeltaEvent() {
return currentGatewayDeltaEvent;
}
@Override
public void setCurrentGatewayDeltaEvent(GatewayDeltaEvent currentGatewayDeltaEvent) {
this.currentGatewayDeltaEvent = currentGatewayDeltaEvent;
}
@Override
public void setOwner(Object manager) {
if (manager instanceof DeltaSessionManager) {
DeltaSessionManager<?> sessionManager = (DeltaSessionManager<?>) manager;
this.manager = sessionManager;
initializeRegion(sessionManager);
hasDelta = false;
applyRemotely = false;
enableGatewayDeltaReplication = sessionManager.getEnableGatewayDeltaReplication();
setOwnerDeprecated(sessionManager);
// Initialize transient variables
if (listeners == null) {
listeners = new ArrayList<>();
}
if (notes == null) {
notes = new Hashtable<>();
}
contextName = sessionManager.getContextName();
} else {
throw new IllegalArgumentException(this + ": The Manager must be an AbstractManager");
}
}
@SuppressWarnings("deprecation")
private void setOwnerDeprecated(DeltaSessionManager<?> sessionManager) {
preferDeserializedForm = sessionManager.getPreferDeserializedForm();
}
private void checkBackingCacheAvailable() {
if (!((SessionManager) getManager()).isBackingCacheAvailable()) {
throw new IllegalStateException("No backing cache server is available.");
}
}
@Override
public void setAttribute(String name, Object value, boolean notify) {
checkBackingCacheAvailable();
synchronized (changeLock) {
// Serialize the value
final byte[] serializedValue = value == null ? null : serialize(value);
// Store the attribute locally
if (preferDeserializedForm) {
if (notify) {
deserializeAttributeInternal(name);
}
super.setAttribute(name, value, true);
} else {
super.setAttribute(name, serializedValue, true);
}
// super.setAttribute above performed a removeAttribute for a value which was null once
// deserialized.
if (value == null) {
return;
}
// Create the update attribute message
DeltaSessionAttributeEvent event =
new DeltaSessionUpdateAttributeEvent(name, serializedValue);
queueAttributeEvent(event, true);
// Distribute the update
if (!isCommitEnabled()) {
putInRegion(getOperatingRegion(), true, null);
}
}
}
@Override
public void removeAttribute(String name, boolean notify) {
checkBackingCacheAvailable();
if (expired) {
return;
}
synchronized (changeLock) {
if (notify && preferDeserializedForm) {
deserializeAttributeInternal(name);
}
// Remove the attribute locally
super.removeAttribute(name, notify);
// Create the destroy attribute message
DeltaSessionAttributeEvent event = new DeltaSessionDestroyAttributeEvent(name);
queueAttributeEvent(event, true);
// Distribute the update
if (!isCommitEnabled()) {
putInRegion(getOperatingRegion(), true, null);
}
}
}
@Override
protected void removeAttributeInternal(String name, boolean notify) {
if (notify && preferDeserializedForm) {
deserializeAttributeInternal(name);
}
super.removeAttributeInternal(name, notify);
}
protected Object getAttributeInternal(final String name) {
if (null == name) {
return null;
}
return getAttributes().get(name);
}
protected void setAttributeInternal(String name, Object value) {
if (null == name) {
return;
}
getAttributes().put(name, value);
}
@Override
public Object getAttribute(String name) {
if (name == null) {
return null;
}
checkBackingCacheAvailable();
Object value = deserializeAttribute(name, super.getAttribute(name), preferDeserializedForm);
// Touch the session region if necessary. This is an asynchronous operation
// that prevents the session region from prematurely expiring a session that
// is only getting attributes.
getDeltaSessionManager().addSessionToTouch(getId());
return value;
}
protected void deserializeAttributeInternal(final String name) {
deserializeAttribute(name, getAttributeInternal(name), true);
}
private Object deserializeAttribute(final String name, final Object value, final boolean store) {
// If the attribute is a byte[] (meaning it came from the server),
// deserialize it and add it to attributes map before returning it.
if (value instanceof byte[]) {
try {
final Object deserialized = BlobHelper.deserializeBlob((byte[]) value);
if (deserialized == null) {
removeAttributeInternal(name, false);
return null;
}
if (store) {
setAttributeInternal(name, deserialized);
}
return deserialized;
} catch (final Exception e) {
getDeltaSessionManager().getLogger().warn(
this + ": Attribute named " + name
+ " contains a byte[] that cannot be deserialized due to the following exception",
e);
}
}
return value;
}
private DeltaSessionManager<?> getDeltaSessionManager() {
return (DeltaSessionManager<?>) getManager();
}
Object getAttributeWithoutDeserialize(String name) {
return super.getAttribute(name);
}
@Override
public void invalidate() {
super.invalidate();
getDeltaSessionManager().getStatistics().incSessionsInvalidated();
}
@Override
public void processExpired() {
DeltaSessionManager<?> manager = getDeltaSessionManager();
if (manager != null && manager.getLogger() != null && manager.getLogger().isDebugEnabled()) {
getDeltaSessionManager().getLogger().debug(this + ": Expired");
}
// Set expired (so region.destroy is not called again)
setExpired(true);
// Do expire processing
super.expire(true);
// Update statistics
if (manager != null) {
manager.getStatistics().incSessionsExpired();
}
}
@Override
public void expire(boolean notify) {
if (notify) {
getOperatingRegion().destroy(getId(), this);
} else {
super.expire(false);
}
}
@Override
public void setMaxInactiveInterval(int interval) {
super.setMaxInactiveInterval(interval);
if (!isCommitEnabled() && id != null) {
putInRegion(getOperatingRegion(), true, null);
}
}
@Override
public void localUpdateAttribute(String name, Object value) {
if (manager == null) {
// Name cannot be null
if (name == null) {
throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull"));
}
// Null value is the same as removeAttribute()
if (value == null) {
removeAttribute(name);
return;
}
// Validate our current state
if (!isValidInternal()) {
throw new IllegalStateException(
sm.getString("standardSession.setAttribute.ise", getIdInternal()));
}
// Replace or add this attribute
getAttributes().put(name, value);
} else {
// don't do notification since this is a replication
super.setAttribute(name, value, false);
}
}
@Override
public void localDestroyAttribute(String name) {
super.removeAttribute(name, false); // don't do notification since this is a replication
}
@Override
public void applyAttributeEvents(Region<String, DeltaSessionInterface> region,
List<DeltaSessionAttributeEvent> events) {
for (DeltaSessionAttributeEvent event : events) {
event.apply(this);
queueAttributeEvent(event, false);
}
putInRegion(region, false, true);
}
private void initializeRegion(DeltaSessionManager<?> sessionManager) {
// Get the session region name
sessionRegionName = sessionManager.getRegionName();
// Get the operating region.
// If a P2P manager is used, then this will be a local region fronting the
// session region if local cache is enabled; otherwise, it will be the
// session region itself.
// If a CS manager is used, it will be the session proxy region.
operatingRegion = uncheckedCast(sessionManager.getSessionCache().getOperatingRegion());
if (sessionManager.getLogger().isDebugEnabled()) {
sessionManager.getLogger().debug(this + ": Set operating region: " + operatingRegion);
}
}
void queueAttributeEvent(DeltaSessionAttributeEvent event,
boolean checkAddToCurrentGatewayDelta) {
// Add to current gateway delta if necessary
if (checkAddToCurrentGatewayDelta) {
// If the manager has enabled gateway delta replication and is a P2P
// manager, the GatewayDeltaForwardCacheListener will be invoked in this
// VM. Add the event to the currentDelta.
if (enableGatewayDeltaReplication && getDeltaSessionManager().isPeerToPeer()) {
// If commit is not enabled, add the event to the current batch; else,
// the current batch will be initialized to the events in the queue will
// be added at commit time.
if (!isCommitEnabled()) {
List<DeltaSessionAttributeEvent> events = new ArrayList<>();
events.add(event);
currentGatewayDeltaEvent =
new DeltaSessionAttributeEventBatch(sessionRegionName, id, events);
}
}
}
addEventToEventQueue(event);
}
private void putInRegion(Region<String, DeltaSessionInterface> region, boolean applyRemotely,
Object callbackArgument) {
hasDelta = true;
this.applyRemotely = applyRemotely;
region.put(id, this, callbackArgument);
eventQueue.clear();
}
@Override
public void commit() {
if (!isValidInternal()) {
throw new IllegalStateException("commit: Session " + getId() + " already invalidated");
}
// (STRING_MANAGER.getString("deltaSession.commit.ise", getId()));
synchronized (changeLock) {
// Jens - there used to be a check to only perform this if the queue is
// empty, but we want this to always run so that the lastAccessedTime
// will be updated even when no attributes have been changed.
if (enableGatewayDeltaReplication && getDeltaSessionManager().isPeerToPeer()) {
setCurrentGatewayDeltaEvent(
new DeltaSessionAttributeEventBatch(sessionRegionName, id, eventQueue));
}
hasDelta = true;
applyRemotely = true;
putInRegion(getOperatingRegion(), true, null);
eventQueue.clear();
}
}
@Override
public void abort() {
synchronized (changeLock) {
eventQueue.clear();
}
}
private void setExpired(boolean expired) {
this.expired = expired;
}
@Override
public boolean getExpired() {
return expired;
}
@Override
public String getContextName() {
return contextName;
}
@Override
public boolean hasDelta() {
return hasDelta;
}
@Override
public void toDelta(DataOutput out) throws IOException {
// Write whether to apply the changes to another DS if necessary
out.writeBoolean(applyRemotely);
// Write the events
DataSerializer.writeArrayList(eventQueue, out);
out.writeLong(lastAccessedTime);
out.writeInt(maxInactiveInterval);
}
@Override
public void fromDelta(DataInput in) throws IOException, InvalidDeltaException {
// Read whether to apply the changes to another DS if necessary
applyRemotely = in.readBoolean();
// Read the events
List<DeltaSessionAttributeEvent> events;
try {
events = DataSerializer.readArrayList(in);
} catch (ClassNotFoundException e) {
throw new InvalidDeltaException(e);
}
// This allows for backwards compatibility with 2.1 clients
if (((InputStream) in).available() > 0) {
lastAccessedTime = in.readLong();
maxInactiveInterval = in.readInt();
}
// Iterate and apply the events
for (DeltaSessionAttributeEvent event : events) {
event.apply(this);
}
// Add the events to the gateway delta region if necessary
if (enableGatewayDeltaReplication && applyRemotely) {
setCurrentGatewayDeltaEvent(
new DeltaSessionAttributeEventBatch(sessionRegionName, id, events));
}
// Access it to set the last accessed time. End access it to set not new.
access();
endAccess();
}
@Override
public void toData(DataOutput out) throws IOException {
// Write the StandardSession state
DataSerializer.writeString(id, out);
out.writeLong(creationTime);
out.writeLong(lastAccessedTime);
out.writeLong(thisAccessedTime);
out.writeInt(maxInactiveInterval);
out.writeBoolean(isNew);
out.writeBoolean(isValid);
DataSerializer.writeObject(getSerializedAttributes(), out);
DataSerializer.writeByteArray(getSerializedPrincipal(), out);
// Write the DeltaSession state
out.writeBoolean(enableGatewayDeltaReplication);
DataSerializer.writeString(sessionRegionName, out);
DataSerializer.writeString(contextName, out);
}
@Override
public void fromData(DataInput in) throws IOException, ClassNotFoundException {
// Read the StandardSession state
id = DataSerializer.readString(in);
creationTime = in.readLong();
lastAccessedTime = in.readLong();
thisAccessedTime = in.readLong();
maxInactiveInterval = in.readInt();
isNew = in.readBoolean();
isValid = in.readBoolean();
readInAttributes(in);
serializedPrincipal = DataSerializer.readByteArray(in);
// Read the DeltaSession state
enableGatewayDeltaReplication = in.readBoolean();
sessionRegionName = DataSerializer.readString(in);
// This allows for backwards compatibility with 2.1 clients
if (((InputStream) in).available() > 0) {
contextName = DataSerializer.readString(in);
}
// Initialize the transients if necessary
if (listeners == null) {
listeners = new ArrayList<>();
}
if (notes == null) {
notes = new Hashtable<>();
}
}
private void readInAttributes(DataInput in) throws IOException, ClassNotFoundException {
ConcurrentHashMap<Object, Object> map = DataSerializer.readObject(in);
try {
Field field = getAttributesFieldObject();
field.set(this, map);
} catch (IllegalAccessException e) {
logError(e);
throw new IllegalStateException(e);
}
}
private Field getAttributesFieldObject() {
return cachedField;
}
private void logError(Exception e) {
final DeltaSessionManager<?> deltaSessionManager = getDeltaSessionManager();
if (deltaSessionManager != null) {
deltaSessionManager.getLogger().error(e);
}
}
@Override
public int getSizeInBytes() {
int size = 0;
Enumeration<String> attributeNames = uncheckedCast(getAttributeNames());
while (attributeNames.hasMoreElements()) {
// Don't use getAttribute() because we don't want to deserialize the value.
Object value = getAttributeWithoutDeserialize(attributeNames.nextElement());
if (value instanceof byte[]) {
size += ((byte[]) value).length;
}
}
return size;
}
private Map<String, byte[]> getSerializedAttributes() {
// Iterate the values and serialize them if necessary before sending them to the server. This
// makes the application classes unnecessary on the server.
Map<String, byte[]> serializedAttributes = new ConcurrentHashMap<>();
for (Map.Entry<String, Object> entry : getAttributes().entrySet()) {
Object value = entry.getValue();
byte[] serializedValue = value instanceof byte[] ? (byte[]) value : serialize(value);
serializedAttributes.put(entry.getKey(), serializedValue);
}
return serializedAttributes;
}
protected Map<String, Object> getAttributes() {
try {
Field field = getAttributesFieldObject();
return uncheckedCast(field.get(this));
} catch (IllegalAccessException e) {
logError(e);
}
throw new IllegalStateException("Unable to access attributes field");
}
byte[] serialize(Object obj) {
byte[] serializedValue = null;
try {
serializedValue = serializeViaBlobHelper(obj);
} catch (IOException e) {
String builder = this + ": Object " + obj
+ " cannot be serialized due to the following exception";
getDeltaSessionManager().getLogger().warn(
builder, e);
}
return serializedValue;
}
byte[] serializeViaBlobHelper(Object obj) throws IOException {
return BlobHelper.serializeToBlob(obj);
}
@Override
public String toString() {
return "DeltaSession[" + "id=" + getId()
+ "; context=" + contextName + "; sessionRegionName="
+ sessionRegionName + "; operatingRegionName="
+ (getOperatingRegion() == null ? "unset" : getOperatingRegion().getFullPath())
+ "]";
}
// Helper methods to enable better unit testing
DeltaSessionFacade getNewFacade(DeltaSessionInterface fSession) {
return AccessController.doPrivileged(
(PrivilegedAction<DeltaSessionFacade>) () -> new DeltaSessionFacade(fSession));
}
boolean isPackageProtectionEnabled() {
return SecurityUtil.isPackageProtectionEnabled();
}
void addEventToEventQueue(DeltaSessionAttributeEvent event) {
eventQueue.add(event);
}
}