/*
* 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.ignite.cache.websession;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.internal.websession.WebSessionEntity;
import org.apache.ignite.marshaller.Marshaller;
import org.jetbrains.annotations.Nullable;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Session implementation that uses internal entity, which stores binary attributes,
* for safe caching and processing on remote nodes.
*/
class WebSessionV2 implements HttpSession {
/** Empty session context. */
@SuppressWarnings("deprecation")
private static final HttpSessionContext EMPTY_SES_CTX = new HttpSessionContext() {
@Nullable @Override public HttpSession getSession(String id) {
return null;
}
@Override public Enumeration<String> getIds() {
return Collections.enumeration(Collections.<String>emptyList());
}
};
/** Placeholder for removed attribute. */
private static final Object REMOVED_ATTR = new Object();
/** Servlet context. */
@GridToStringExclude
private final ServletContext ctx;
/** Entity that holds binary attributes. */
private WebSessionEntity entity;
/** Attributes. */
protected Map<String, Object> attrs;
/** Timestamp that shows when this object was created. (Last access time from user request) */
private final long accessTime;
/** Cached session TTL since last query. */
private int maxInactiveInterval;
/** Flag indicates if {@link #maxInactiveInterval} waiting for update in cache. */
private boolean maxInactiveIntervalChanged;
/** New session flag. */
private boolean isNew;
/** Session invalidation flag. */
private boolean invalidated;
/** Grid marshaller. */
private final Marshaller marsh;
/** Original session to delegate invalidation. */
private final HttpSession genuineSes;
/**
* Constructs new web session.
*
* @param id Session ID.
* @param ses Session.
* @param isNew Is new flag.
* @param ctx Servlet context.
* @param entity Entity.
* @param marsh Marshaller.
*/
WebSessionV2(final String id, final @Nullable HttpSession ses, final boolean isNew, final ServletContext ctx,
@Nullable WebSessionEntity entity, final Marshaller marsh) {
assert id != null;
assert marsh != null;
assert ctx != null;
assert ses != null || entity != null;
this.marsh = marsh;
this.ctx = ctx;
this.isNew = isNew;
this.genuineSes = ses;
accessTime = System.currentTimeMillis();
if (entity == null) {
entity = new WebSessionEntity(id, ses.getCreationTime(), accessTime,
ses.getMaxInactiveInterval());
}
this.entity = entity;
maxInactiveInterval = entity.maxInactiveInterval();
if (ses != null) {
final Enumeration<String> names = ses.getAttributeNames();
while (names.hasMoreElements()) {
final String name = names.nextElement();
attributes().put(name, ses.getAttribute(name));
}
}
}
/** {@inheritDoc} */
@Override public long getCreationTime() {
assertValid();
return entity.createTime();
}
/** {@inheritDoc} */
@Override public String getId() {
return entity.id();
}
/**
* @return Session ID without throwing exception.
*/
public String id() {
return entity.id();
}
/** {@inheritDoc} */
@Override public long getLastAccessedTime() {
assertValid();
return accessTime;
}
/** {@inheritDoc} */
@Override public ServletContext getServletContext() {
return ctx;
}
/** {@inheritDoc} */
@Override public void setMaxInactiveInterval(final int interval) {
maxInactiveInterval = interval;
maxInactiveIntervalChanged = true;
}
/**
* @return {@code True} if {@link #setMaxInactiveInterval(int)} was invoked.
*/
public boolean isMaxInactiveIntervalChanged() {
return maxInactiveIntervalChanged;
}
/** {@inheritDoc} */
@Override public int getMaxInactiveInterval() {
return maxInactiveInterval;
}
/** {@inheritDoc} */
@SuppressWarnings("deprecation")
@Override public HttpSessionContext getSessionContext() {
return EMPTY_SES_CTX;
}
/** {@inheritDoc} */
@Override public Object getAttribute(final String name) {
assertValid();
Object attr = attributes().get(name);
if (attr == REMOVED_ATTR)
return null;
if (attr == null) {
final byte[] bytes = entity.attributes().get(name);
if (bytes != null) {
// deserialize
try {
attr = unmarshal(bytes);
}
catch (IOException e) {
throw new IgniteException(e);
}
attributes().put(name, attr);
}
}
return attr;
}
/** {@inheritDoc} */
@Override public Object getValue(final String name) {
return getAttribute(name);
}
/** {@inheritDoc} */
@Override public void setAttribute(final String name, final Object val) {
assertValid();
if (val == null)
removeAttribute(name);
else
attributes().put(name, val);
}
/** {@inheritDoc} */
@Override public void putValue(final String name, final Object val) {
setAttribute(name, val);
}
/** {@inheritDoc} */
@Override public Enumeration<String> getAttributeNames() {
assertValid();
return Collections.enumeration(attributeNames());
}
/**
* @return Set of attribute names.
*/
private Set<String> attributeNames() {
if (!F.isEmpty(attrs)) {
final Set<String> names = new HashSet<>(entity.attributes().size() + attrs.size());
names.addAll(entity.attributes().keySet());
for (final Map.Entry<String, Object> entry : attrs.entrySet()) {
if (entry != REMOVED_ATTR)
names.add(entry.getKey());
}
return names;
}
return entity.attributes().keySet();
}
/** {@inheritDoc} */
@Override public String[] getValueNames() {
assertValid();
final Set<String> names = attributeNames();
return names.toArray(new String[names.size()]);
}
/** {@inheritDoc} */
@Override public void removeAttribute(final String name) {
assertValid();
attributes().put(name, REMOVED_ATTR);
}
/** {@inheritDoc} */
@Override public void removeValue(final String name) {
removeAttribute(name);
}
/** {@inheritDoc} */
@Override public void invalidate() {
assertValid();
if (genuineSes != null) {
try {
genuineSes.invalidate();
}
catch (IllegalStateException ignored) {
// Already invalidated, keep going.
}
}
invalidated = true;
}
/** {@inheritDoc} */
@Override public boolean isNew() {
assertValid();
return isNew;
}
/**
* @return Marshaled updates or empty map if no params.
* @throws IOException
*/
public Map<String, byte[]> binaryUpdatesMap() throws IOException {
final Map<String, Object> map = attributes();
if (F.isEmpty(map))
return Collections.emptyMap();
final Map<String, byte[]> res = new HashMap<>(map.size());
for (final Map.Entry<String, Object> entry : map.entrySet()) {
Object val = entry.getValue() == REMOVED_ATTR ? null : entry.getValue();
res.put(entry.getKey(), marshal(val));
}
return res;
}
/**
* Unmarshal object.
*
* @param bytes Data.
* @param <T> Expected type.
* @return Unmarshaled object.
* @throws IOException If unarshaling failed.
*/
@Nullable private <T> T unmarshal(final byte[] bytes) throws IOException {
if (marsh != null) {
try {
return U.unmarshal(marsh, bytes, getClass().getClassLoader());
}
catch (IgniteCheckedException e) {
throw new IOException(e);
}
}
return null;
}
/**
* Marshal object.
*
* @param obj Object to marshal.
* @return Binary data.
* @throws IOException If marshaling failed.
*/
@Nullable private byte[] marshal(final Object obj) throws IOException {
if (marsh != null) {
try {
return U.marshal(marsh, obj);
}
catch (IgniteCheckedException e) {
throw new IOException(e);
}
}
return null;
}
/**
* Marshal all attributes and save to serializable entity.
*/
public WebSessionEntity marshalAttributes() throws IOException {
final WebSessionEntity marshaled = new WebSessionEntity(getId(), entity.createTime(), accessTime,
maxInactiveInterval);
for (final Map.Entry<String, Object> entry : attributes().entrySet())
marshaled.putAttribute(entry.getKey(), marshal(entry.getValue()));
return marshaled;
}
/**
* @return Session attributes.
*/
private Map<String, Object> attributes() {
if (attrs == null)
attrs = new HashMap<>();
return attrs;
}
/**
* @return {@code True} if session wasn't invalidated.
*/
public boolean isValid() {
return !invalidated;
}
/**
* Throw {@link IllegalStateException} if session was invalidated.
*/
private void assertValid() {
if (invalidated)
throw new IllegalStateException("Session was invalidated.");
}
}