/*
* Copyright (c) 2011 PonySDK
* Owners:
* Luciano Broussal <luciano.broussal AT gmail.com>
* Mathieu Barbier <mathieu.barbier AT gmail.com>
* Nicolas Ciaravola <nicolas.ciaravola.pro AT gmail.com>
*
* WebSite:
* http://code.google.com/p/pony-sdk/
*
* 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.
*/
package com.ponysdk.core.server.application;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ponysdk.core.model.ClientToServerModel;
import com.ponysdk.core.model.HandlerModel;
import com.ponysdk.core.model.ServerToClientModel;
import com.ponysdk.core.server.servlet.CommunicationSanityChecker;
import com.ponysdk.core.server.stm.Txn;
import com.ponysdk.core.server.stm.TxnContext;
import com.ponysdk.core.ui.basic.DataListener;
import com.ponysdk.core.ui.basic.PCookies;
import com.ponysdk.core.ui.basic.PHistory;
import com.ponysdk.core.ui.basic.PObject;
import com.ponysdk.core.ui.eventbus.BroadcastEventHandler;
import com.ponysdk.core.ui.eventbus.Event;
import com.ponysdk.core.ui.eventbus.EventHandler;
import com.ponysdk.core.ui.eventbus.HandlerRegistration;
import com.ponysdk.core.ui.eventbus.RootEventBus;
import com.ponysdk.core.ui.eventbus.StreamHandler;
import com.ponysdk.core.ui.eventbus2.EventBus;
import com.ponysdk.core.ui.statistic.TerminalDataReceiver;
import com.ponysdk.core.writer.ModelWriter;
/**
* <p>
* Provides a way to identify a user across more than one page request or visit to a Web site and to
* store information about that user.
* </p>
* <p>
* There is ONE unique UIContext for each screen displayed. Each UIContext is bound to the current
* {@link Application} .
* </p>
*/
public class UIContext {
private static final ThreadLocal<UIContext> currentContext = new ThreadLocal<>();
private static final Logger log = LoggerFactory.getLogger(UIContext.class);
private static final AtomicInteger uiContextCount = new AtomicInteger();
private final int ID;
private final Application application;
private final ReentrantLock lock = new ReentrantLock();
private final Map<String, Object> attributes = new HashMap<>();
private int objectCounter = 1;
private final PObjectWeakHashMap pObjectWeakReferences = new PObjectWeakHashMap();
private int streamRequestCounter = 0;
private final Map<Integer, StreamHandler> streamListenerByID = new HashMap<>();
private final PHistory history = new PHistory();
private final RootEventBus rootEventBus = new RootEventBus();
private final EventBus newEventBus = new EventBus();
private final PCookies cookies = new PCookies();
private final CommunicationSanityChecker communicationSanityChecker;
private final List<UIContextListener> uiContextListeners = new ArrayList<>();
private final TxnContext context;
private final Set<DataListener> listeners = Collections.newSetFromMap(new ConcurrentHashMap<>());
private TerminalDataReceiver terminalDataReceiver;
private boolean living = true;
private final BoundedLinkedList<Long> pings = new BoundedLinkedList<>(10);
public UIContext(final TxnContext context) {
this.application = context.getApplication();
this.ID = uiContextCount.incrementAndGet();
this.context = context;
this.communicationSanityChecker = new CommunicationSanityChecker(this);
this.communicationSanityChecker.start();
}
public static final UIContext get() {
return currentContext.get();
}
public static final void remove() {
currentContext.remove();
}
public static final void setCurrent(final UIContext uiContext) {
currentContext.set(uiContext);
}
public static final HandlerRegistration addHandler(final Event.Type type, final EventHandler handler) {
return getRootEventBus().addHandler(type, handler);
}
public static final void removeHandler(final Event.Type type, final EventHandler handler) {
getRootEventBus().removeHandler(type, handler);
}
public static final HandlerRegistration addHandlerToSource(final Event.Type type, final Object source,
final EventHandler handler) {
return getRootEventBus().addHandlerToSource(type, source, handler);
}
public static final void removeHandlerFromSource(final Event.Type type, final Object source, final EventHandler handler) {
getRootEventBus().removeHandlerFromSource(type, source, handler);
}
public static final void fireEvent(final Event<? extends EventHandler> event) {
getRootEventBus().fireEvent(event);
}
public static final void fireEventFromSource(final Event<? extends EventHandler> event, final Object source) {
getRootEventBus().fireEventFromSource(event, source);
}
public static final void addHandler(final BroadcastEventHandler handler) {
getRootEventBus().addHandler(handler);
}
public static final void removeHandler(final BroadcastEventHandler handler) {
getRootEventBus().removeHandler(handler);
}
public static final RootEventBus getRootEventBus() {
return get().rootEventBus;
}
public static final EventBus getNewEventBus() {
return get().newEventBus;
}
public int getID() {
return ID;
}
public void addDataListener(final DataListener listener) {
listeners.add(listener);
}
public void removeDataListener(final DataListener listener) {
listeners.remove(listener);
}
public void execute(final Runnable runnable) {
if (log.isDebugEnabled()) log.debug("Pushing to #" + this);
if (UIContext.get() != this) {
begin();
try {
final Txn txn = Txn.get();
txn.begin(context);
try {
runnable.run();
txn.commit();
} catch (final Throwable e) {
log.error("Cannot process client instruction", e);
txn.rollback();
}
} finally {
end();
}
} else {
runnable.run();
}
}
public void pushToClient(final List<Object> data) {
execute(() -> fireOnData(data));
}
public void pushToClient(final Object data) {
execute(() -> fireOnData(data));
}
private void fireOnData(final List<Object> data) {
if (listeners.isEmpty()) return;
try {
listeners.forEach(listener -> data.forEach(listener::onData));
} catch (final Throwable e) {
log.error("Cannot send data", e);
}
}
private void fireOnData(final Object data) {
if (listeners.isEmpty()) return;
try {
listeners.forEach(listener -> listener.onData(data));
} catch (final Throwable e) {
log.error("Cannot send data", e);
}
}
public void fireClientData(final JsonObject jsonObject) {
if (jsonObject.containsKey(ClientToServerModel.TYPE_HISTORY.toStringValue())) {
if (history != null) {
history.fireHistoryChanged(jsonObject.getString(ClientToServerModel.TYPE_HISTORY.toStringValue()));
}
} else {
final JsonValue jsonValue = jsonObject.get(ClientToServerModel.OBJECT_ID.toStringValue());
int objectID;
if (ValueType.NUMBER.equals(jsonValue.getValueType())) {
objectID = ((JsonNumber) jsonValue).intValue();
} else if (ValueType.STRING.equals(jsonValue.getValueType())) {
objectID = Integer.parseInt(((JsonString) jsonValue).getString());
} else {
log.error("unknown reference from the browser. Unable to execute instruction: " + jsonObject);
return;
}
//Cookies
if (objectID == 0) {
cookies.onClientData(jsonObject);
} else {
final PObject object = getObject(objectID);
if (object == null) {
log.error("unknown reference from the browser. Unable to execute instruction: " + jsonObject);
if (jsonObject.containsKey(ClientToServerModel.PARENT_OBJECT_ID.toStringValue())) {
final int parentObjectID = jsonObject.getJsonNumber(ClientToServerModel.PARENT_OBJECT_ID.toStringValue())
.intValue();
final PObject gcObject = pObjectWeakReferences.get(parentObjectID);
log.warn(String.valueOf(gcObject));
}
return;
}
if (terminalDataReceiver != null) terminalDataReceiver.onDataReceived(object, jsonObject);
object.onClientData(jsonObject);
}
}
}
public void setClientDataOutput(final TerminalDataReceiver clientDataOutput) {
this.terminalDataReceiver = clientDataOutput;
}
public void begin() {
lock.lock();
UIContext.setCurrent(this);
}
public void end() {
UIContext.remove();
lock.unlock();
}
public int nextID() {
return objectCounter++;
}
public void registerObject(final PObject object) {
pObjectWeakReferences.put(object.getID(), object);
}
public <T> T getObject(final int objectID) {
return (T) pObjectWeakReferences.get(objectID);
}
public void stackStreamRequest(final StreamHandler streamListener) {
final int streamRequestID = nextStreamRequestID();
final ModelWriter writer = Txn.getWriter();
writer.beginObject();
writer.write(ServerToClientModel.TYPE_ADD_HANDLER, -1);
writer.write(ServerToClientModel.HANDLER_TYPE, HandlerModel.HANDLER_STREAM_REQUEST.getValue());
writer.write(ServerToClientModel.STREAM_REQUEST_ID, streamRequestID);
writer.write(ServerToClientModel.APPLICATION_ID, getApplication().getId());
writer.endObject();
streamListenerByID.put(streamRequestID, streamListener);
}
public void stackEmbededStreamRequest(final StreamHandler streamListener, final int objectID) {
final int streamRequestID = nextStreamRequestID();
final ModelWriter writer = Txn.getWriter();
writer.beginObject();
writer.write(ServerToClientModel.TYPE_ADD_HANDLER, objectID);
writer.write(ServerToClientModel.HANDLER_TYPE, HandlerModel.HANDLER_EMBEDED_STREAM_REQUEST.getValue());
writer.write(ServerToClientModel.STREAM_REQUEST_ID, streamRequestID);
writer.write(ServerToClientModel.APPLICATION_ID, getApplication().getId());
writer.endObject();
streamListenerByID.put(streamRequestID, streamListener);
}
private int nextStreamRequestID() {
return streamRequestCounter++;
}
public StreamHandler removeStreamListener(final int streamID) {
return streamListenerByID.remove(streamID);
}
public PHistory getHistory() {
return history;
}
public PCookies getCookies() {
return cookies;
}
public void close() {
final ModelWriter writer = Txn.getWriter();
writer.beginObject();
writer.write(ServerToClientModel.DESTROY_CONTEXT, null);
writer.endObject();
}
/**
* Binds an object to this session, using the name specified. If an object
* of the same name is already bound to the session, the object is replaced.
* <p>
* If the value passed in is null, this has the same effect as calling
* <code>removeAttribute()<code>.
*
* @param name
* the name to which the object is bound; cannot be null
* @param value
* the object to be bound
*/
public void setAttribute(final String name, final Object value) {
if (value == null) removeAttribute(name);
else attributes.put(name, value);
}
/**
* Removes the object bound with the specified name from this session. If
* the session does not have an object bound with the specified name, this
* method does nothing.
*
* @param name
* the name of the object to remove from this session
*/
public Object removeAttribute(final String name) {
return attributes.remove(name);
}
/**
* Returns the object bound with the specified name in this session, or <code>null</code> if no
* object is bound under the name.
*
* @param name
* a string specifying the name of the object
* @return the object with the specified name
*/
public <T> T getAttribute(final String name) {
return (T) attributes.get(name);
}
public Application getApplication() {
return application;
}
public void notifyMessageReceived() {
communicationSanityChecker.onMessageReceived();
}
public void onDestroy() {
begin();
try {
doDestroy();
} finally {
end();
}
}
void destroyFromApplication() {
begin();
try {
living = false;
communicationSanityChecker.stop();
uiContextListeners.forEach(listener -> listener.onUIContextDestroyed(this));
context.close();
} finally {
end();
}
}
public void destroy() {
begin();
try {
doDestroy();
context.close();
} finally {
end();
}
}
private void doDestroy() {
living = false;
communicationSanityChecker.stop();
application.unregisterUIContext(ID);
uiContextListeners.forEach(listener -> listener.onUIContextDestroyed(this));
}
public void sendHeartBeat() {
begin();
try {
context.sendHeartBeat();
} catch (final Throwable e) {
log.error("Cannot send server heart beat to client", e);
} finally {
end();
}
}
public void sendRoundTrip() {
begin();
try {
context.sendRoundTrip();
} catch (final Throwable e) {
log.error("Cannot send server round trip to client", e);
} finally {
end();
}
}
public void addUIContextListener(final UIContextListener listener) {
uiContextListeners.add(listener);
}
public boolean isLiving() {
return living;
}
public TxnContext getContext() {
return context;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final UIContext uiContext = (UIContext) o;
return ID == uiContext.ID;
}
@Override
public int hashCode() {
return Objects.hash(ID);
}
@Override
public String toString() {
return "UIContext [" + application + ", uiContextID=" + ID + ", living=" + living + "]";
}
public void enableCommunicationChecker(final boolean enable) {
communicationSanityChecker.enableCommunicationChecker(enable);
}
public void addPingValue(final long pingValue) {
pings.add(pingValue);
}
/**
* Get an average latency from the last 10 measurements
*/
public double getLatency() {
return pings.stream().mapToLong(Long::longValue).average().orElse(0);
}
private static final class BoundedLinkedList<E> extends LinkedList<E> {
private final int limit;
public BoundedLinkedList(final int limit) {
this.limit = limit;
}
@Override
public boolean add(final E e) {
if (this.size() == this.limit) this.removeFirst();
return super.add(e);
}
}
}