com.webfirmframework.wffweb.server.page.BrowserPage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of wffweb Show documentation
Show all versions of wffweb Show documentation
A java framework to develop web applications
/*
* Copyright 2014-2020 Web Firm Framework
*
* 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.webfirmframework.wffweb.server.page;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.webfirmframework.wffweb.InvalidUsageException;
import com.webfirmframework.wffweb.InvalidValueException;
import com.webfirmframework.wffweb.NotRenderedException;
import com.webfirmframework.wffweb.NullValueException;
import com.webfirmframework.wffweb.PushFailedException;
import com.webfirmframework.wffweb.server.page.js.WffJsFile;
import com.webfirmframework.wffweb.tag.html.AbstractHtml;
import com.webfirmframework.wffweb.tag.html.Html;
import com.webfirmframework.wffweb.tag.html.TagNameConstants;
import com.webfirmframework.wffweb.tag.html.TagUtil;
import com.webfirmframework.wffweb.tag.html.attribute.Defer;
import com.webfirmframework.wffweb.tag.html.attribute.Nonce;
import com.webfirmframework.wffweb.tag.html.attribute.Src;
import com.webfirmframework.wffweb.tag.html.attribute.Type;
import com.webfirmframework.wffweb.tag.html.attribute.core.AbstractAttribute;
import com.webfirmframework.wffweb.tag.html.attribute.event.EventAttribute;
import com.webfirmframework.wffweb.tag.html.attribute.event.ServerAsyncMethod;
import com.webfirmframework.wffweb.tag.html.attribute.listener.AttributeValueChangeListener;
import com.webfirmframework.wffweb.tag.html.html5.attribute.global.DataWffId;
import com.webfirmframework.wffweb.tag.html.programming.Script;
import com.webfirmframework.wffweb.tag.htmlwff.NoTag;
import com.webfirmframework.wffweb.tag.repository.TagRepository;
import com.webfirmframework.wffweb.util.HashUtil;
import com.webfirmframework.wffweb.util.WffBinaryMessageUtil;
import com.webfirmframework.wffweb.util.data.NameValue;
import com.webfirmframework.wffweb.wffbm.data.WffBMObject;
/**
* @author WFF
* @since 2.0.0
*/
public abstract class BrowserPage implements Serializable {
// if this class' is refactored then SecurityClassConstants should be
// updated.
private static final long serialVersionUID = 1_0_1L;
private static final Logger LOGGER = Logger
.getLogger(BrowserPage.class.getName());
public static final String WFF_INSTANCE_ID = "wffInstanceId";
private static final boolean PRODUCTION_MODE = true;
private final String instanceId = UUID.randomUUID().toString();
private AttributeValueChangeListener valueChangeListener;
private Map tagByWffId;
private volatile AbstractHtml rootTag;
private volatile boolean wsWarningDisabled;
private final Map sessionIdWsListeners = new ConcurrentHashMap<>();
private final Deque wsListeners = new ConcurrentLinkedDeque<>();
private volatile WebSocketPushListener wsListener;
private DataWffId wffScriptTagId;
/**
* it's true by default since 3.0.1
*/
private boolean enableDeferOnWffScript = true;
private Nonce nonceForWffScriptTag;
private boolean autoremoveWffScript = true;
private volatile boolean renderInvoked;
// ConcurrentLinkedQueue give better performance than ConcurrentLinkedDeque
// on benchmark
private final Deque wffBMBytesQueue = new ConcurrentLinkedDeque<>();
// ConcurrentLinkedQueue give better performance than ConcurrentLinkedDeque
// on benchmark
private final Queue wffBMBytesHoldPushQueue = new ConcurrentLinkedQueue<>();
private static final Security ACCESS_OBJECT = new Security();
// by default the push queue should be enabled
private boolean pushQueueEnabled = true;
// by default the pushQueueOnNewWebSocketListener should be enabled
private boolean pushQueueOnNewWebSocketListener = true;
private final AtomicInteger holdPush = new AtomicInteger(0);
private final Map serverMethods = new ConcurrentHashMap<>();
private boolean removeFromBrowserContextOnTabClose = true;
private boolean removePrevFromBrowserContextOnTabInit = true;
private int wsHeartbeatInterval = -1;
private int wsReconnectInterval = -1;
private static int wsDefaultHeartbeatInterval = 25_000;
private static int wsDefaultReconnectInterval = 2_000;
private final LongAdder pushQueueSize = new LongAdder();
// there will be only one thread waiting for the lock so fairness must be
// false and fairness may decrease the lock time
private final ReentrantLock pushWffBMBytesQueueLock = new ReentrantLock(
false);
// there will be only one thread waiting for the lock so fairness must be
// false and fairness may decrease the lock time
private final ReentrantLock unholdPushLock = new ReentrantLock(false);
private final AtomicReference waitingThreadRef = new AtomicReference<>();
private volatile TagRepository tagRepository;
// to make it GC friendly, it is made as static
private static final ThreadLocal PALYLOAD_PROCESSOR_TL = new ThreadLocal<>();
// NB: this non-static initialization makes BrowserPage and PayloadProcessor
// never to get GCd. It leads to memory leak. It seems to be a bug.
// private final ThreadLocal PALYLOAD_PROCESSOR_TL =
// ThreadLocal
// .withInitial(() -> new PayloadProcessor(this, true));
// for security purpose, the class name should not be modified
private static final class Security implements Serializable {
private static final long serialVersionUID = 1L;
private Security() {
}
}
/**
* To specify (by removeFromContext method) when to remove
* {@code BrowserPage} instance from {@code BrowserPageContext}.
*
* @author WFF
* @since 2.1.4
*/
public static enum On {
/**
* to remove the current {@code BrowserPage} instance from
* {@code BrowserPageContext} when the tab/window is closed.
*/
TAB_CLOSE,
/**
* To remove the previous {@code BrowserPage} instance opened in the
* same tab when new {@code BrowserPage} is requested by the tab.
*/
INIT_REMOVE_PREVIOUS;
}
public abstract String webSocketUrl();
/**
* @param wsListener
* @since 2.0.0
* @author WFF
*/
public final void setWebSocketPushListener(
final WebSocketPushListener wsListener) {
this.wsListener = wsListener;
if (rootTag != null) {
rootTag.getSharedObject().setActiveWSListener(wsListener != null,
ACCESS_OBJECT);
}
if (pushQueueOnNewWebSocketListener) {
pushWffBMBytesQueue();
}
}
/**
* adds the WebSocket listener for the given WebSocket session
*
* @param sessionId
* the unique id of WebSocket session
* @param wsListener
* @since 2.1.0
* @author WFF
*/
public final void addWebSocketPushListener(final String sessionId,
final WebSocketPushListener wsListener) {
sessionIdWsListeners.put(sessionId, wsListener);
// should be in the first of this queue as it could provide the latest
// reliable ws connection
wsListeners.push(wsListener);
this.wsListener = wsListener;
if (rootTag != null) {
rootTag.getSharedObject().setActiveWSListener(wsListener != null,
ACCESS_OBJECT);
}
if (pushQueueOnNewWebSocketListener) {
pushWffBMBytesQueue();
}
}
/**
* removes the WebSocket listener added for this WebSocket session
*
* @param sessionId
* the unique id of WebSocket session
* @since 2.1.0
* @author WFF
*/
public final void removeWebSocketPushListener(final String sessionId) {
final WebSocketPushListener removedListener = sessionIdWsListeners
.remove(sessionId);
// remove all
while (wsListeners.remove(removedListener)) {
}
wsListener = wsListeners.peek();
if (rootTag != null) {
rootTag.getSharedObject().setActiveWSListener(wsListener != null,
ACCESS_OBJECT);
}
}
public final WebSocketPushListener getWsListener() {
return wsListener;
}
final void push(final NameValue... nameValues) {
push(new ClientTasksWrapper(
ByteBuffer.wrap(WffBinaryMessageUtil.VERSION_1
.getWffBinaryMessageBytes(nameValues))));
}
/**
* @param multiTasks
* @return the wrapper class holding the byteBuffers of multiple tasks
*/
final ClientTasksWrapper pushAndGetWrapper(
final Queue> multiTasks) {
final ByteBuffer[] tasks = new ByteBuffer[multiTasks.size()];
int index = 0;
Collection taskNameValues;
while ((taskNameValues = multiTasks.poll()) != null) {
final NameValue[] nameValues = taskNameValues
.toArray(new NameValue[taskNameValues.size()]);
final ByteBuffer byteBuffer = ByteBuffer
.wrap(WffBinaryMessageUtil.VERSION_1
.getWffBinaryMessageBytes(nameValues));
tasks[index] = byteBuffer;
index++;
}
final ClientTasksWrapper clientTasks = new ClientTasksWrapper(tasks);
push(clientTasks);
return clientTasks;
}
private void push(final ClientTasksWrapper clientTasks) {
if (holdPush.get() > 0) {
// add method internally calls offer method in ConcurrentLinkedQueue
wffBMBytesHoldPushQueue.offer(clientTasks);
} else {
if (!wffBMBytesHoldPushQueue.isEmpty()) {
copyCachedBMBytesToMainQ();
}
// add method internally calls offer which internally
// calls offerLast method in ConcurrentLinkedQueue
if (wffBMBytesQueue.offerLast(clientTasks)) {
pushQueueSize.increment();
}
}
}
private void pushWffBMBytesQueue() {
if (wsListener != null) {
// hasQueuedThreads internally uses transient volatile Node
// so it must be fine for production use but
// TODO verify it in deep if it is good for production
if (!pushWffBMBytesQueueLock.hasQueuedThreads()
&& !wffBMBytesQueue.isEmpty()) {
try {
waitingThreadRef.set(Thread.currentThread());
pushWffBMBytesQueueLock.lock();
// wsPushInProgress must be implemented here and it is very
// important because multiple threads should not push
// simultaneously
// from same wffBMBytesQueue which will cause incorrect
// order of
// push
ClientTasksWrapper clientTask = wffBMBytesQueue.poll();
if (clientTask != null) {
AtomicReferenceArray byteBuffers;
final Thread currentThread = Thread.currentThread();
do {
pushQueueSize.decrement();
try {
byteBuffers = clientTask.tasks();
if (byteBuffers != null) {
final int length = byteBuffers.length();
for (int i = 0; i < length; i++) {
final ByteBuffer byteBuffer = byteBuffers
.get(i);
if (byteBuffer != null) {
wsListener.push(byteBuffer);
}
byteBuffers.set(i, null);
}
clientTask.nullifyTasks();
}
} catch (final PushFailedException e) {
if (pushQueueEnabled && wffBMBytesQueue
.offerFirst(clientTask)) {
pushQueueSize.increment();
}
break;
} catch (final IllegalStateException
| NullPointerException e) {
if (wffBMBytesQueue.offerFirst(clientTask)) {
pushQueueSize.increment();
}
break;
}
if (pushWffBMBytesQueueLock.hasQueuedThreads()
&& waitingThreadRef.get()
.getPriority() >= currentThread
.getPriority()) {
break;
}
clientTask = wffBMBytesQueue.poll();
} while (clientTask != null);
}
} finally {
pushWffBMBytesQueueLock.unlock();
}
}
} else {
if (LOGGER.isLoggable(Level.WARNING) && !wsWarningDisabled) {
LOGGER.warning(
"There is no WebSocket listener set, set it with BrowserPage#setWebSocketPushListener method.");
}
}
}
final DataWffId getNewDataWffId() {
return rootTag.getSharedObject().getNewDataWffId(ACCESS_OBJECT);
}
/**
* This method will be remove later. Use {@code webSocketMessaged}.
*
* @param message
* the bytes the received in onmessage
* @since 2.0.0
* @author WFF
* @deprecated alternative method webSocketMessaged is available for the
* same job.
*
*/
@Deprecated
public final void websocketMessaged(final byte[] message) {
webSocketMessaged(message);
}
/**
* @param message
* the bytes the received in onmessage
* @since 2.1.0
* @author WFF
*/
public final void webSocketMessaged(final byte[] message) {
try {
// TODO minimum number of an empty bm message length is 4
// below that length is not a valid bm message so check
// message.length < 4
// later if there is such requirement
if (message.length == 0) {
// should not proceed if the message.length is zero because
// to avoid exception if the client sends an empty message just
// for ping
return;
}
executeWffBMTask(message);
} catch (final Exception e) {
if (!PRODUCTION_MODE) {
e.printStackTrace();
}
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE,
"Could not process this data received from client.", e);
}
}
}
/**
* Invokes just before {@link BrowserPage#render()} method. This is an empty
* method in BrowserPage. Override and use. This method invokes only once
* per object in all of its life time.
*
* @since 3.0.1
*/
protected void beforeRender() {
// NOP override and use
}
/**
* Override and use this method to render html content to the client browser
* page. This method invokes only once per object in all of its life time.
*
* @return the object of {@link Html} class which needs to be displayed in
* the client browser page.
* @author WFF
*/
public abstract AbstractHtml render();
/**
* Invokes after {@link BrowserPage#render()} method. This is an empty
* method in BrowserPage. Override and use. This method invokes only once
* per object in all of its life time.
*
* @param rootTag
* the rootTag returned by {@link BrowserPage#render()}
* method.
*
* @since 3.0.1
*/
protected void afterRender(final AbstractHtml rootTag) {
// NOP override and use
}
/**
* Invokes before any of the {@code toHtmlString} or {@code toOutputStream}
* methods invoked. This is an empty method in BrowserPage. Override and use
* it. This method will invoke every time before any of the following
* methods is called, {@code toHtmlString}, {@code toOutputStream} etc..
*
*
* @param rootTag
* the rootTag returned by {@link BrowserPage#render()}
* method.
*
* @since 3.0.1
*/
protected void beforeToHtml(final AbstractHtml rootTag) {
// NOP override and use
}
/**
* Invokes after any of the {@code toHtmlString} or {@code toOutputStream}
* methods invoked. This is an empty method in BrowserPage. Override and use
* it. This method will invoke every time after any of the following methods
* is called, {@code toHtmlString}, {@code toOutputStream} etc..
*
*
* @param rootTag
* the rootTag returned by {@link BrowserPage#render()}
* method.
*
* @since 3.0.1
*/
protected void afterToHtml(final AbstractHtml rootTag) {
// NOP override and use
}
/**
* @param nameValues
* @throws UnsupportedEncodingException
* throwing this exception will be
* removed in future version
* because its internal
* implementation will never make
* this exception due to the code
* changes since 3.0.1.
*/
private void invokeAsychMethod(final List nameValues)
throws UnsupportedEncodingException {
//@formatter:off
// invoke method task format :-
// { "name": task_byte, "values" : [invoke_method_byte_from_Task_enum]}, { "name": data-wff-id, "values" : [ event_attribute_name ]}
// { "name": 2, "values" : [[0]]}, { "name":"C55", "values" : ["onclick"]}
//@formatter:on
final NameValue wffTagIdAndAttrName = nameValues.get(1);
final byte[] intBytes = new byte[wffTagIdAndAttrName.getName().length
- 1];
System.arraycopy(wffTagIdAndAttrName.getName(), 1, intBytes, 0,
intBytes.length);
final String wffTagId = new String(wffTagIdAndAttrName.getName(), 0, 1,
StandardCharsets.UTF_8)
+ WffBinaryMessageUtil.getIntFromOptimizedBytes(intBytes);
final byte[][] values = wffTagIdAndAttrName.getValues();
final String eventAttrName = new String(values[0],
StandardCharsets.UTF_8);
WffBMObject wffBMObject = null;
if (values.length > 1) {
final byte[] wffBMObjectBytes = values[1];
wffBMObject = new WffBMObject(wffBMObjectBytes, true);
}
final AbstractHtml methodTag = tagByWffId.get(wffTagId);
if (methodTag != null) {
final AbstractAttribute attributeByName = methodTag
.getAttributeByName(eventAttrName);
if (attributeByName != null) {
if (attributeByName instanceof EventAttribute) {
final EventAttribute eventAttr = (EventAttribute) attributeByName;
final ServerAsyncMethod serverAsyncMethod = eventAttr
.getServerAsyncMethod();
final ServerAsyncMethod.Event event = new ServerAsyncMethod.Event(
methodTag, attributeByName,
eventAttr.getServerSideData());
final WffBMObject returnedObject = serverAsyncMethod
.asyncMethod(wffBMObject, event);
final String jsPostFunctionBody = eventAttr
.getJsPostFunctionBody();
if (jsPostFunctionBody != null) {
final NameValue invokePostFunTask = Task.INVOKE_POST_FUNCTION
.getTaskNameValue();
final NameValue nameValue = new NameValue();
// name as function body string and value at
// zeroth index as
// wffBMObject bytes
nameValue.setName(jsPostFunctionBody
.getBytes(StandardCharsets.UTF_8));
if (returnedObject != null) {
nameValue.setValues(new byte[][] {
returnedObject.buildBytes(true) });
}
push(invokePostFunTask, nameValue);
if (holdPush.get() == 0) {
pushWffBMBytesQueue();
}
}
} else {
LOGGER.severe(attributeByName
+ " is NOT instanceof EventAttribute");
}
} else {
LOGGER.severe(
"no event attribute found for " + attributeByName);
}
} else {
if (!PRODUCTION_MODE) {
LOGGER.severe("No tag found for wffTagId " + wffTagId);
}
}
}
/**
* @param nameValues
* @throws UnsupportedEncodingException
* throwing this exception will be
* removed in future version
* because its internal
* implementation will never make
* this exception due to the code
* changes since 3.0.1.
*/
private void removeBrowserPageFromContext(final List nameValues)
throws UnsupportedEncodingException {
//@formatter:off
// invoke custom server method task format :-
// { "name": task_byte, "values" : [remove_browser_page_byte_from_Task_enum]},
// { "name": wff-instance-id-bytes, "values" : []}
//@formatter:on
final NameValue instanceIdNameValue = nameValues.get(1);
final String instanceIdToRemove = new String(
instanceIdNameValue.getName(), StandardCharsets.UTF_8);
BrowserPageContext.INSTANCE.removeBrowserPage(getInstanceId(),
instanceIdToRemove);
}
/**
* @param nameValues
* @throws UnsupportedEncodingException
* throwing this exception will be
* removed in future version
* because its internal
* implementation will never make
* this exception due to the code
* changes since 3.0.1.
*/
private void invokeCustomServerMethod(final List nameValues)
throws UnsupportedEncodingException {
//@formatter:off
// invoke custom server method task format :-
// { "name": task_byte, "values" : [invoke_custom_server_method_byte_from_Task_enum]},
// { "name": server method name bytes, "values" : [ wffBMObject bytes ]}
// { "name": callback function id bytes, "values" : [ ]}
//@formatter:on
final NameValue methodNameAndArg = nameValues.get(1);
final String methodName = new String(methodNameAndArg.getName(),
StandardCharsets.UTF_8);
final ServerMethod serverMethod = serverMethods.get(methodName);
if (serverMethod != null) {
final byte[][] values = methodNameAndArg.getValues();
WffBMObject wffBMObject = null;
if (values.length > 0) {
wffBMObject = new WffBMObject(values[0], true);
}
final WffBMObject returnedObject = serverMethod
.getServerAsyncMethod().asyncMethod(wffBMObject,
new ServerAsyncMethod.Event(methodName,
serverMethod.getServerSideData()));
String callbackFunId = null;
if (nameValues.size() > 2) {
final NameValue callbackFunNameValue = nameValues.get(2);
callbackFunId = new String(callbackFunNameValue.getName(),
StandardCharsets.UTF_8);
}
if (callbackFunId != null) {
final NameValue invokeCallbackFuncTask = Task.INVOKE_CALLBACK_FUNCTION
.getTaskNameValue();
final NameValue nameValue = new NameValue();
nameValue.setName(
callbackFunId.getBytes(StandardCharsets.UTF_8));
if (returnedObject != null) {
nameValue.setValues(
new byte[][] { returnedObject.buildBytes(true) });
}
push(invokeCallbackFuncTask, nameValue);
if (holdPush.get() == 0) {
pushWffBMBytesQueue();
}
}
} else {
LOGGER.warning(methodName
+ " doesn't exist, please add it as browserPage.addServerMethod(\""
+ methodName + "\", serverAsyncMethod)");
}
}
/**
* executes the task in the given wff binary message.
* For WFF authors :- Make sure that the passing {@code message} is not
* empty while consuming this method, just as made conditional checking in
* {@code BrowserPage#webSocketMessaged(byte[])} method.
*
* @since 2.0.0
* @author WFF
* @throws UnsupportedEncodingException
* throwing this exception will be
* removed in future version
* because its internal
* implementation will never make
* this exception due to the code
* changes since 3.0.1.
*/
private void executeWffBMTask(final byte[] message)
throws UnsupportedEncodingException {
final List nameValues = WffBinaryMessageUtil.VERSION_1
.parse(message);
final NameValue task = nameValues.get(0);
final byte taskValue = task.getValues()[0][0];
if (Task.TASK.getValueByte() == task.getName()[0]) {
// IM stands for Invoke Method
if (taskValue == Task.INVOKE_ASYNC_METHOD.getValueByte()) {
invokeAsychMethod(nameValues);
} else if (taskValue == Task.INVOKE_CUSTOM_SERVER_METHOD
.getValueByte()) {
invokeCustomServerMethod(nameValues);
} else if (taskValue == Task.REMOVE_BROWSER_PAGE.getValueByte()) {
removeBrowserPageFromContext(nameValues);
}
}
}
private void addAttrValueChangeListener(final AbstractHtml abstractHtml) {
if (valueChangeListener == null) {
valueChangeListener = new AttributeValueChangeListenerImpl(this,
tagByWffId);
}
abstractHtml.getSharedObject()
.setValueChangeListener(valueChangeListener, ACCESS_OBJECT);
}
private void addDataWffIdAttribute(final AbstractHtml abstractHtml) {
final Deque> childrenStack = new ArrayDeque<>();
// passed 2 instead of 1 because the load factor is 0.75f
final Set initialSet = new LinkedHashSet<>(2);
initialSet.add(abstractHtml);
childrenStack.push(initialSet);
Set children;
while ((children = childrenStack.poll()) != null) {
for (final AbstractHtml child : children) {
if (TagUtil.isTagged(child)) {
if (child.getDataWffId() == null) {
child.setDataWffId(getNewDataWffId());
}
tagByWffId.put(child.getDataWffId().getValue(), child);
}
final Set subChildren = child
.getChildren(ACCESS_OBJECT);
if (subChildren != null && subChildren.size() > 0) {
childrenStack.push(subChildren);
}
}
}
}
private void embedWffScriptIfRequired(final AbstractHtml abstractHtml,
final String wsUrlWithInstanceId) {
if (wffScriptTagId != null
&& tagByWffId.containsKey(wffScriptTagId.getValue())) {
// no need to add script tag if it exists in the ui
return;
}
final Deque> childrenStack = new ArrayDeque<>();
// passed 2 instead of 1 because the load factor is 0.75f
final Set initialSet = new LinkedHashSet<>(2);
initialSet.add(abstractHtml);
childrenStack.push(initialSet);
boolean headAndbodyTagMissing = true;
Set children;
outerLoop: while ((children = childrenStack.poll()) != null) {
for (final AbstractHtml child : children) {
if ((enableDeferOnWffScript
&& TagNameConstants.HEAD.equals(child.getTagName()))
|| (!enableDeferOnWffScript && TagNameConstants.BODY
.equals(child.getTagName()))) {
headAndbodyTagMissing = false;
wffScriptTagId = getNewDataWffId();
final Script script = new Script(null,
new Type(Type.TEXT_JAVASCRIPT));
script.setDataWffId(wffScriptTagId);
final String wffJs = WffJsFile.getAllOptimizedContent(
wsUrlWithInstanceId, getInstanceId(),
removePrevFromBrowserContextOnTabInit,
removeFromBrowserContextOnTabClose,
(wsHeartbeatInterval > 0 ? wsHeartbeatInterval
: wsDefaultHeartbeatInterval),
(wsReconnectInterval > 0 ? wsReconnectInterval
: wsDefaultReconnectInterval),
autoremoveWffScript);
if (enableDeferOnWffScript) {
// byes are in UTF-8 so charset=utf-8 is explicitly
// specified
// Defer must be first argument
script.addAttributes(new Defer(null), new Src(
"data:text/javascript;charset=utf-8;base64,"
+ HashUtil.base64FromUtf8Bytes(
wffJs.getBytes(
StandardCharsets.UTF_8))));
} else {
new NoTag(script, wffJs);
}
if (nonceForWffScriptTag != null) {
script.addAttributes(nonceForWffScriptTag);
}
// to avoid invoking listener
child.addChild(ACCESS_OBJECT, script, false);
// ConcurrentHashMap cannot contain null as value
tagByWffId.put(wffScriptTagId.getValue(), script);
break outerLoop;
}
final Set subChildren = child
.getChildren(ACCESS_OBJECT);
if (subChildren != null && subChildren.size() > 0) {
childrenStack.push(subChildren);
}
}
}
if (headAndbodyTagMissing) {
wffScriptTagId = getNewDataWffId();
final Script script = new Script(null,
new Type(Type.TEXT_JAVASCRIPT));
script.setDataWffId(wffScriptTagId);
final String wffJs = WffJsFile.getAllOptimizedContent(
wsUrlWithInstanceId, getInstanceId(),
removePrevFromBrowserContextOnTabInit,
removeFromBrowserContextOnTabClose,
(wsHeartbeatInterval > 0 ? wsHeartbeatInterval
: wsDefaultHeartbeatInterval),
(wsReconnectInterval > 0 ? wsReconnectInterval
: wsDefaultReconnectInterval),
autoremoveWffScript);
if (enableDeferOnWffScript) {
// byes are in UTF-8 so charset=utf-8 is explicitly specified
// Defer must be first argument
script.addAttributes(new Defer(null),
new Src("data:text/javascript;charset=utf-8;base64,"
+ HashUtil.base64FromUtf8Bytes(wffJs
.getBytes(StandardCharsets.UTF_8))));
} else {
new NoTag(script, wffJs);
}
if (nonceForWffScriptTag != null) {
script.addAttributes(nonceForWffScriptTag);
}
// to avoid invoking listener
abstractHtml.addChild(ACCESS_OBJECT, script, false);
// ConcurrentHashMap cannot contain null as value
tagByWffId.put(wffScriptTagId.getValue(), script);
}
}
private void addChildTagAppendListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setChildTagAppendListener(
new ChildTagAppendListenerImpl(this, ACCESS_OBJECT, tagByWffId),
ACCESS_OBJECT);
}
private void addChildTagRemoveListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setChildTagRemoveListener(
new ChildTagRemoveListenerImpl(this, ACCESS_OBJECT, tagByWffId),
ACCESS_OBJECT);
}
private void addAttributeAddListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setAttributeAddListener(
new AttributeAddListenerImpl(this, ACCESS_OBJECT),
ACCESS_OBJECT);
}
private void addAttributeRemoveListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setAttributeRemoveListener(
new AttributeRemoveListenerImpl(this, ACCESS_OBJECT,
tagByWffId),
ACCESS_OBJECT);
}
private void addInnerHtmlAddListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setInnerHtmlAddListener(
new InnerHtmlAddListenerImpl(this, ACCESS_OBJECT, tagByWffId),
ACCESS_OBJECT);
}
private void addInsertTagsBeforeListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setInsertTagsBeforeListener(
new InsertTagsBeforeListenerImpl(this, ACCESS_OBJECT,
tagByWffId),
ACCESS_OBJECT);
}
private void addInsertAfterListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setInsertAfterListener(
new InsertAfterListenerImpl(this, ACCESS_OBJECT, tagByWffId),
ACCESS_OBJECT);
}
private void addReplaceListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setReplaceListener(
new ReplaceListenerImpl(this, ACCESS_OBJECT, tagByWffId),
ACCESS_OBJECT);
}
private void addWffBMDataUpdateListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setWffBMDataUpdateListener(
new WffBMDataUpdateListenerImpl(this, ACCESS_OBJECT),
ACCESS_OBJECT);
}
private void addWffBMDataDeleteListener(final AbstractHtml abstractHtml) {
abstractHtml.getSharedObject().setWffBMDataDeleteListener(
new WffBMDataDeleteListenerImpl(this, ACCESS_OBJECT),
ACCESS_OBJECT);
}
private void addPushQueue(final AbstractHtml rootTag) {
rootTag.getSharedObject().setPushQueue(() -> {
if (holdPush.get() == 0) {
pushWffBMBytesQueue();
}
}, ACCESS_OBJECT);
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @return {@code String} equalent to the html string of the tag including
* the child tags.
*
* @author WFF
*/
public String toHtmlString() {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final String htmlString = rootTag.toHtmlString(true);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return htmlString;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method. rebuilds the html string of the tag including the child
* tags/values if parameter is true, otherwise returns the html string
* prebuilt and kept in the cache.
*
* @param rebuild
* true to rebuild & false to return previously built
* string.
* @return {@code String} equalent to the html string of the tag including
* the child tags.
* @since 2.1.4
* @author WFF
*/
public String toHtmlString(final boolean rebuild) {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final String htmlString = rootTag.toHtmlString(rebuild);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return htmlString;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param charset
* the charset
* @return {@code String} equalent to the html string of the tag including
* the child tags.
* @author WFF
*/
public String toHtmlString(final String charset) {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final String htmlString = rootTag.toHtmlString(true, charset);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return htmlString;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method. rebuilds the html string of the tag including the child
* tags/values if parameter is true, otherwise returns the html string
* prebuilt and kept in the cache.
*
* @param rebuild
* true to rebuild & false to return previously built
* string.
* @param charset
* the charset to set for the returning value, eg:
* {@code StandardCharsets.UTF_8.name()}
* @return {@code String} equalent to the html string of the tag including
* the child tags.
*
* @since 2.1.4
* @author WFF
*/
public String toHtmlString(final boolean rebuild, final String charset) {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final String htmlString = rootTag.toHtmlString(rebuild, charset);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return htmlString;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @return the total number of bytes written
* @since 2.1.4 void toOutputStream
* @since 2.1.8 int toOutputStream
* @throws IOException
*/
public int toOutputStream(final OutputStream os) throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, true);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @param rebuild
* true to rebuild & false to write previously built
* bytes.
* @return the total number of bytes written
*
* @throws IOException
* @since 2.1.4 void toOutputStream
* @since 2.1.8 int toOutputStream
*
*/
public int toOutputStream(final OutputStream os, final boolean rebuild)
throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, rebuild);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @param rebuild
* true to rebuild & false to write previously
* built bytes.
* @param flushOnWrite
* true to flush on each write to OutputStream
* @return the total number of bytes written
*
* @throws IOException
* @since 3.0.2
*
*/
public int toOutputStream(final OutputStream os, final boolean rebuild,
final boolean flushOnWrite) throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, rebuild,
flushOnWrite);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @param charset
* the charset
* @return the total number of bytes written
* @throws IOException
* @since 2.1.4 void toOutputStream
* @since 2.1.8 int toOutputStream
*/
public int toOutputStream(final OutputStream os, final String charset)
throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, true, charset);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @param charset
* the charset
* @param flushOnWrite
* true to flush on each write to OutputStream
* @return the total number of bytes written
* @throws IOException
* @since 3.0.2
*/
public int toOutputStream(final OutputStream os, final Charset charset,
final boolean flushOnWrite) throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, true, charset,
flushOnWrite);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @param rebuild
* true to rebuild & false to write previously built
* bytes.
* @param charset
* the charset
* @return the total number of bytes written
* @throws IOException
* @since 2.1.4 void toOutputStream
* @since 2.1.8 int toOutputStream
*
*/
public int toOutputStream(final OutputStream os, final boolean rebuild,
final String charset) throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, rebuild, charset);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
/**
* NB: this method should not be called under {@link BrowserPage#render()}
* method because this method internally calls {@link BrowserPage#render()}
* method.
*
* @param os
* the object of {@code OutputStream} to write to.
* @param rebuild
* true to rebuild & false to write previously
* built bytes.
* @param charset
* the charset
* @param flushOnWrite
* true to flush on each write to OutputStream
* @return the total number of bytes written
* @throws IOException
* @since 3.0.2
*
*/
public int toOutputStream(final OutputStream os, final boolean rebuild,
final Charset charset, final boolean flushOnWrite)
throws IOException {
initAbstractHtml();
wsWarningDisabled = true;
beforeToHtml(rootTag);
wsWarningDisabled = false;
final int totalWritten = rootTag.toOutputStream(os, rebuild, charset,
flushOnWrite);
wsWarningDisabled = true;
afterToHtml(rootTag);
wsWarningDisabled = false;
return totalWritten;
}
private void initAbstractHtml() {
if (rootTag == null) {
synchronized (this) {
if (rootTag == null) {
if (renderInvoked) {
throw new InvalidUsageException(
"This method cannot be called here because this method is called by render or its child method.");
}
renderInvoked = true;
beforeRender();
rootTag = render();
if (rootTag == null) {
renderInvoked = false;
throw new NullValueException(
"render must return an instance of AbstractHtml, eg:- new Html(null);");
}
tagByWffId = rootTag.getSharedObject()
.initTagByWffId(ACCESS_OBJECT);
addDataWffIdAttribute(rootTag);
// attribute value change listener
// should be added only after adding data-wff-id attribute
addAttrValueChangeListener(rootTag);
addChildTagAppendListener(rootTag);
addChildTagRemoveListener(rootTag);
addAttributeAddListener(rootTag);
addAttributeRemoveListener(rootTag);
addInnerHtmlAddListener(rootTag);
addInsertAfterListener(rootTag);
addInsertTagsBeforeListener(rootTag);
addReplaceListener(rootTag);
addWffBMDataUpdateListener(rootTag);
addWffBMDataDeleteListener(rootTag);
addPushQueue(rootTag);
wsWarningDisabled = true;
afterRender(rootTag);
wsWarningDisabled = false;
} else {
unholdPushLock.lock();
try {
wffBMBytesQueue.clear();
pushQueueSize.reset();
} finally {
unholdPushLock.unlock();
}
}
}
} else {
unholdPushLock.lock();
try {
wffBMBytesQueue.clear();
pushQueueSize.reset();
} finally {
unholdPushLock.unlock();
}
}
final String webSocketUrl = webSocketUrl();
if (webSocketUrl == null) {
throw new NullValueException(
"webSocketUrl must return valid WebSocket url");
}
final String wsUrlWithInstanceId = webSocketUrl.indexOf('?') == -1
? webSocketUrl + "?wffInstanceId=" + getInstanceId()
: webSocketUrl + "&wffInstanceId=" + getInstanceId();
embedWffScriptIfRequired(rootTag, wsUrlWithInstanceId);
}
/**
* @return a unique id for this instance
* @since 2.0.0
* @author WFF
*/
public final String getInstanceId() {
return instanceId;
}
/**
* By default, it is set as true.
*
* @return the pushQueueEnabled
* @since 2.0.2
*/
public final boolean isPushQueueEnabled() {
return pushQueueEnabled;
}
/**
* If the server could not push any server updates it will be put in the
* queue and when it tries to push in the next time it will first push
* updates from this queue. By default, it is set as true.
*
* @param enabledPushQueue
* the enabledPushQueue to set
* @since 2.0.2
*/
public final void setPushQueueEnabled(final boolean enabledPushQueue) {
pushQueueEnabled = enabledPushQueue;
}
/**
* @param methodName
* @param serverAsyncMethod
* @since 2.1.0
* @author WFF
*/
public final void addServerMethod(final String methodName,
final ServerAsyncMethod serverAsyncMethod) {
serverMethods.put(methodName,
new ServerMethod(serverAsyncMethod, null));
}
/**
* @param methodName
* @param serverAsyncMethod
* @param serverSideData
* this object will be available in the event
* of serverAsyncMethod.asyncMethod
* @since 3.0.2
* @author WFF
*/
public final void addServerMethod(final String methodName,
final ServerAsyncMethod serverAsyncMethod,
final Object serverSideData) {
serverMethods.put(methodName,
new ServerMethod(serverAsyncMethod, serverSideData));
}
/**
* removes the method from
*
* @param methodName
* @since 2.1.0
* @author WFF
*/
public final void removeServerMethod(final String methodName) {
serverMethods.remove(methodName);
}
/**
* performs action provided by {@code BrowserPageAction}.
*
* @param actionByteBuffer
* The ByteBuffer object taken from
* {@code BrowserPageAction} .Eg:-
* {@code BrowserPageAction.RELOAD.getActionByteBuffer();}
* @since 2.1.0
* @author WFF
*/
public final void performBrowserPageAction(
final ByteBuffer actionByteBuffer) {
push(new ClientTasksWrapper(actionByteBuffer));
if (holdPush.get() == 0) {
pushWffBMBytesQueue();
}
}
/**
* @return the pushQueueOnNewWebSocketListener true if it's enabled
* otherwise false. By default it's set as true.
* @since 2.1.1
*/
public final boolean isPushQueueOnNewWebSocketListener() {
return pushQueueOnNewWebSocketListener;
}
/**
* By default it's set as true. If it's enabled then the wffbmBytesQueue
* will be pushed when new webSocket listener is added/set.
*
* @param pushQueueOnNewWebSocketListener
* the
* pushQueueOnNewWebSocketListener
* to set. Pass true to enable
* this option and false to
* disable this option.
* @since 2.1.1
*/
public final void setPushQueueOnNewWebSocketListener(
final boolean pushQueueOnNewWebSocketListener) {
this.pushQueueOnNewWebSocketListener = pushQueueOnNewWebSocketListener;
}
/**
*
* @return the holdPush true if the push is on hold
* @since 2.1.3
*/
public final boolean isHoldPush() {
return holdPush.get() > 0;
}
/**
* holds push if not already on hold until unholdPush is called Usage :-
*
*
* try {
* browserPage.holdPush();
*
* for (AbstractHtml tag : tags) {
* tag.removeAttributes("style");
* }
* // other tag manipulations
* } finally {
* browserPage.unholdPush();
* }
*
*
* @since 2.1.3
* @author WFF
*/
public final void holdPush() {
holdPush.incrementAndGet();
}
/**
* unholds push if not already unheld. Usage :-
*
*
* try {
* browserPage.holdPush();
*
* for (AbstractHtml tag : tags) {
* tag.removeAttributes("style");
* }
* // other tag manipulations
* } finally {
* browserPage.unholdPush();
* }
*
*
* @since 2.1.3
* @author WFF
*/
public final void unholdPush() {
if (holdPush.get() > 0) {
holdPush.decrementAndGet();
if (copyCachedBMBytesToMainQ()) {
pushWffBMBytesQueue();
}
}
}
private boolean copyCachedBMBytesToMainQ() {
boolean copied = false;
if (!unholdPushLock.hasQueuedThreads()) {
unholdPushLock.lock();
try {
ClientTasksWrapper clientTask = wffBMBytesHoldPushQueue.poll();
if (clientTask != null) {
final NameValue invokeMultipleTasks = Task
.getTaskOfTasksNameValue();
final Deque wffBMs = new ArrayDeque<>(
wffBMBytesHoldPushQueue.size() + 1);
AtomicReferenceArray byteBuffers;
do {
byteBuffers = clientTask.tasks();
if (byteBuffers != null) {
final int length = byteBuffers.length();
for (int i = 0; i < length; i++) {
final ByteBuffer wffBM = byteBuffers.get(i);
if (wffBM != null) {
wffBMs.add(wffBM);
}
byteBuffers.set(i, null);
}
clientTask.nullifyTasks();
}
clientTask = wffBMBytesHoldPushQueue.poll();
} while (clientTask != null);
final byte[][] values = new byte[wffBMs.size()][0];
int index = 0;
for (final ByteBuffer eachWffBM : wffBMs) {
values[index] = eachWffBM.array();
index++;
}
invokeMultipleTasks.setValues(values);
wffBMBytesQueue.add(new ClientTasksWrapper(
ByteBuffer.wrap(WffBinaryMessageUtil.VERSION_1
.getWffBinaryMessageBytes(
invokeMultipleTasks))));
pushQueueSize.increment();
copied = true;
}
} finally {
unholdPushLock.unlock();
}
}
return copied;
}
/**
* Gets the size of internal push queue. This size might not be accurate in
* multi-threading environment.
*
* Use case :- Suppose there is a thread in the server which makes real time
* ui changes. But if the end user lost connection and the webSocket is not
* closed connection, in such case the developer can decide whether to make
* any more ui updates from server when the pushQueueSize exceeds a
* particular limit.
*
* @return the size of internal push queue.
* @since 2.1.4
* @author WFF
*/
public final int getPushQueueSize() {
// wffBMBytesQueue.size() is not reliable as
// it's ConcurrentLinkedQueue.
// As per the javadoc ConcurrentLinkedQueue.size is NOT a constant-time
// operation. So to avoid performance degrade of using
// wffBMBytesQueue.size a separate LongAdder is kept to maintain the
// queue size.
return pushQueueSize.intValue();
}
/**
* By default On.TAB_CLOSE and On.INIT_REMOVE_PREVIOUS are enabled.
*
* @param enable
* @param ons
* the instance of On to represent on which browser event
* the browser page needs to be removed.
* @since 2.1.4
* @author WFF
*/
public final void removeFromContext(final boolean enable, final On... ons) {
for (final On on : ons) {
if (On.TAB_CLOSE.equals(on)) {
removeFromBrowserContextOnTabClose = enable;
} else if (On.INIT_REMOVE_PREVIOUS.equals(on)) {
removePrevFromBrowserContextOnTabInit = enable;
}
}
}
/**
* Invokes when this browser page instance is removed from browser page
* context. Override and use this method to stop long running tasks /
* threads.
*
* @since 2.1.4
* @author WFF
*/
protected void removedFromContext() {
// NOP
// To override and use this method
}
/**
* To check if the given tag exists in the UI.
* NB:- This method is valid only if {@code browserPage#toHtmlString} or
* {@code browserPage#toOutputStream} is called at least once in the life
* time.
*
* @param tag
* the tag object to be checked.
* @return true if the given tag contains in the BrowserPage i.e. UI. false
* if the given tag was removed or was not already added in the UI.
* @throws NullValueException
* throws this exception if the given tag
* is null.
* @throws NotRenderedException
* if the {@code BrowserPage} object is not
* rendered. i.e. if
* {@code browserPage#toHtmlString} or
* {@code browserPage#toOutputStream} was
* NOT called at least once in the life
* time.
* @since 2.1.7
* @author WFF
*/
public final boolean contains(final AbstractHtml tag)
throws NullValueException, NotRenderedException {
if (tag == null) {
throw new NullValueException(
"tag object in browserPage.contains(AbstractHtml tag) method cannot be null");
}
if (tagByWffId == null) {
throw new NotRenderedException(
"Could not check its existance. Make sure that you have called browserPage#toHtmlString method atleast once in the life time.");
}
final DataWffId dataWffId = tag.getDataWffId();
if (dataWffId == null) {
return false;
}
return tag.equals(tagByWffId.get(dataWffId.getValue()));
}
/**
* Sets the heartbeat ping interval of webSocket client in milliseconds.
* Give -1 to disable it. By default it's set with -1. It affects only for
* the corresponding {@code BrowserPage} instance from which it is called.
*
* NB:- This method has effect only if it is called before
* {@code BrowserPage#render()} method return. This method can be called
* inside {@code BrowserPage#render()} method to override the default global
* heartbeat interval set by
* {@code BrowserPage#setWebSocketDefultHeartbeatInterval(int)} method.
*
* @param milliseconds
* the heartbeat ping interval of webSocket client
* in milliseconds. Give -1 to disable it.
* @since 2.1.8
* @author WFF
*/
protected final void setWebSocketHeartbeatInterval(final int milliseconds) {
wsHeartbeatInterval = milliseconds;
}
/**
* @return the interval value set by
* {@code BrowserPage#setWebSocketHeartbeatInterval(int)} method.
* @since 2.1.8
* @author WFF
*/
public final int getWebSocketHeartbeatInterval() {
return wsHeartbeatInterval;
}
/**
* Sets the default heartbeat ping interval of webSocket client in
* milliseconds. Give -1 to disable it. It affects globally. By default it's
* set with -1 till wffweb-2.1.8 and Since wffweb-2.1.9 it's 25000ms i.e. 25
* seconds.
* NB:- This method has effect only if it is called before
* {@code BrowserPage#render()} invocation.
*
*
* @param milliseconds
* the heartbeat ping interval of webSocket client
* in milliseconds. Give -1 to disable it
* @since 2.1.8
* @since 2.1.9 the default value is 25000ms i.e. 25 seconds.
* @author WFF
*/
public static final void setWebSocketDefultHeartbeatInterval(
final int milliseconds) {
wsDefaultHeartbeatInterval = milliseconds;
}
/**
* @return the interval value set by
* {@code setWebSocketDefultHeartbeatInterval} method.
* @since 2.1.8
* @author WFF
*/
public static final int getWebSocketDefultHeartbeatInterval() {
return wsDefaultHeartbeatInterval;
}
/**
* Sets the default reconnect interval of webSocket client in milliseconds.
* It affects globally. By default it's set with 2000 ms.
* NB:- This method has effect only if it is called before
* {@code BrowserPage#render()} invocation.
*
*
* @param milliseconds
* the reconnect interval of webSocket client in
* milliseconds. It must be greater than 0.
* @since 2.1.8
* @author WFF
*/
public static final void setWebSocketDefultReconnectInterval(
final int milliseconds) {
if (milliseconds < 1) {
throw new InvalidValueException("The value must be greater than 0");
}
wsDefaultReconnectInterval = milliseconds;
}
/**
* @return the interval value set by
* {@code setWebSocketDefultReconnectInterval} method.
* @since 2.1.8
* @author WFF
*/
public static final int getWebSocketDefultReconnectInterval() {
return wsDefaultReconnectInterval;
}
/**
* Sets the reconnect interval of webSocket client in milliseconds. Give -1
* to disable it. By default it's set with -1. It affects only for the
* corresponding {@code BrowserPage} instance from which it is called.
* NB:- This method has effect only if it is called before
* {@code BrowserPage#render()} method return. This method can be called
* inside {@code BrowserPage#render()} method to override the default global
* WebSocket reconnect interval set by
* {@code BrowserPage#setWebSocketDefultReconnectInterval(int)} method.
*
* @param milliseconds
* the reconnect interval of webSocket client in
* milliseconds. Give -1 to disable it.
* @since 2.1.8
* @author WFF
*/
protected final void setWebSocketReconnectInterval(final int milliseconds) {
wsReconnectInterval = milliseconds;
}
/**
* @return the interval value set by
* {@code BrowserPage#setWebSocketReconnectInterval(int)} method.
* @since 2.1.8
* @author WFF
*/
public final int getWebSocketReconnectInterval() {
return wsReconnectInterval;
}
/**
* Gets the TagRepository to do different tag operations. This tag
* repository is specific to this BrowserPage instance.
*
* @return the TagRepository object to do different tag operations. Or null
* if any one of the BrowserPage#toString or
* BrowserPage#toOutputStream methods is not called.
* @since 2.1.8
* @author WFF
*/
public final TagRepository getTagRepository() {
if (tagRepository == null && rootTag != null) {
synchronized (this) {
if (tagRepository == null) {
tagRepository = new TagRepository(ACCESS_OBJECT, this,
tagByWffId, rootTag);
}
}
}
return tagRepository;
}
/**
* Sets nonce attribute value for wff script.
*
* @param value
* pass value to set nonce value or pass null to remove
* nonce attribute
* @since 3.0.1
*/
protected final void setNonceForWffScript(final String value) {
if (autoremoveWffScript) {
throw new InvalidUsageException(
"Cannot remove while autoremoveWffScript is set as true. Please do setAutoremoveWffScript(false)");
}
if (value != null) {
if (nonceForWffScriptTag == null) {
nonceForWffScriptTag = new Nonce(value);
if (wffScriptTagId != null) {
final AbstractHtml[] ownerTags = wffScriptTagId
.getOwnerTags();
if (ownerTags.length > 0) {
final AbstractHtml wffScript = ownerTags[0];
wffScript.addAttributes(nonceForWffScriptTag);
}
}
} else {
nonceForWffScriptTag.setValue(value);
}
} else {
if (wffScriptTagId != null && nonceForWffScriptTag != null) {
final AbstractHtml[] ownerTags = wffScriptTagId.getOwnerTags();
if (ownerTags.length > 0) {
final AbstractHtml wffScript = ownerTags[0];
wffScript.removeAttributes(nonceForWffScriptTag);
}
}
nonceForWffScriptTag = null;
}
}
/**
* @param algo
* eg: HashUtil.SHA_256
* @return the base64 encoded string of the hash generated with the given
* algo for the wff script (text/javascript) content.
* @since 3.0.1
*/
private String getWffScriptHashInBase64(final String algo) {
initAbstractHtml();
if (wffScriptTagId != null) {
final AbstractHtml[] ownerTags = wffScriptTagId.getOwnerTags();
if (ownerTags.length > 0) {
final AbstractHtml wffScript = ownerTags[0];
final NoTag firstChild = (NoTag) wffScript.getFirstChild();
final String childContent = firstChild.getChildContent();
try {
return HashUtil.hashInBase64(childContent, algo);
} catch (final NoSuchAlgorithmException e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE,
"Make sure that the jdk supports " + algo, e);
}
}
}
}
return null;
}
/**
* Generates and gets the SHA hash of internal wff script as base64 encoded
* string. NB: this method should not be called under
* {@link BrowserPage#render()} method because this method internally calls
* {@link BrowserPage#render()} method.
*
* @return the wff script (text/javascript) content converted to SHA-256
* hash encoded in base64 string.
* @since 3.0.1
*/
public final String getWffScriptSHA256InBase64() {
return getWffScriptHashInBase64(HashUtil.SHA_256);
}
/**
* Generates and gets the SHA hash of internal wff script as base64 encoded
* string. NB: this method should not be called under
* {@link BrowserPage#render()} method because this method internally calls
* {@link BrowserPage#render()} method.
*
* @return the wff script (text/javascript) content converted to SHA-384
* hash encoded in base64 string.
* @since 3.0.1
*/
public final String getWffScriptSHA384InBase64() {
return getWffScriptHashInBase64(HashUtil.SHA_384);
}
/**
* Generates and gets the SHA hash of internal wff script as base64 encoded
* string. NB: this method should not be called under
* {@link BrowserPage#render()} method because this method internally calls
* {@link BrowserPage#render()} method.
*
* @return the wff script (text/javascript) content converted to SHA-512
* hash encoded in base64 string.
* @since 3.0.1
*/
public final String getWffScriptSHA512InBase64() {
return getWffScriptHashInBase64(HashUtil.SHA_512);
}
/**
* It must be called from render method to take effect. By default it's set
* as true. so Content-Security-Policy is implemented then script-src must
* allow data:.
*
* @param enable
* @since 3.0.1
*/
protected final void setEnableDeferOnWffScript(final boolean enable) {
enableDeferOnWffScript = enable;
}
/**
* by default it's true.
*
* @return true if enabled otherwise false
* @since 3.0.1
*/
protected final boolean isEnableDeferOnWffScript() {
return enableDeferOnWffScript;
}
/**
* By default it is true.
*
* @return the current state
* @since 3.0.1
*/
protected final boolean isAutoremoveWffScript() {
return autoremoveWffScript;
}
/**
* Automatically removes the wff script tag after loading it from the ui. By
* default it is true.
*
* @param autoremoveWffScript
* @since 3.0.1
*/
protected final void setAutoremoveWffScript(
final boolean autoremoveWffScript) {
this.autoremoveWffScript = autoremoveWffScript;
}
/**
* Gets the same instance of {@code PayloadProcessor} per caller thread for
* this browser page. This PayloadProcessor can process incoming partial
* bytes from WebSocket. To manually create new PayloadProcessor use new
* PayloadProcessor(browserPage).
*
* @return new instance of PayloadProcessor/thread
* @since 3.0.2
* @deprecated this method call may make deadlock somewhere in the
* application while using multiple threads. Use
* {@code BrowserPage#newPayloadProcessor()}.
*/
@Deprecated
public final PayloadProcessor getPayloadProcessor() {
PayloadProcessor payloadProcessor = PALYLOAD_PROCESSOR_TL.get();
if (payloadProcessor == null) {
payloadProcessor = new PayloadProcessor(this, true);
PALYLOAD_PROCESSOR_TL.set(payloadProcessor);
}
return payloadProcessor;
}
/**
* Removes the current instance of {@code PayloadProcessor} of this caller
* thread for this browser page and new instance will be reinitialized when
* calling {@link #getPayloadProcessor()} by the same thread.
*
* @since 3.0.2
* @deprecated this method call may make deadlock.
*/
@Deprecated
public final void removePayloadProcessor() {
PALYLOAD_PROCESSOR_TL.remove();
}
/**
* Creates and returns new instance of {@code PayloadProcessor} for this
* browser page. This PayloadProcessor can process incoming partial bytes
* from WebSocket. To manually create new PayloadProcessor use new
* PayloadProcessor(browserPage). Use this PayloadProcessor instance
* only under one thread at a time for its complete payload parts.
*
* @return new instance of PayloadProcessor each method call.
* @since 3.0.11
*/
public final PayloadProcessor newPayloadProcessor() {
return new PayloadProcessor(this, true);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy