/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.server.headlessclient; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; /** * A keeper of JS (and other) actions to be applied on a page on an AJAX request or header response. * It keeps JS actions ordered as added in each set except for triggerAjaxUpdate that will be added at the end (see SVY-1328), no matter when it's called. * * @author acostescu */ @SuppressWarnings("nls") public class PageJSActionBuffer { private final List<PageAction> buffer1 = new ArrayList<PageAction>(); private final List<TriggerAjaxUpdateAction> buffer2 = new ArrayList<TriggerAjaxUpdateAction>(); private static int batchID = 1; private static String newBatchID() { return "dd_ab_" + (batchID++); } public static interface PageAction { /** * Applies this page action to the given target, on a ajax request from given page. * @param target * @param childFrameBatchId null if the request came from the page who's buffer contains this action. An unique ID if some other page's request is trying to execute the action. * @return true if the action (was completed successfully and) should be removed from the action buffer. If this action cannot be executed on an ajax request target, returns false. */ boolean apply(AjaxRequestTarget target, String childFrameBatchId); /** * Applies this page action to the given header response (for example onLoad javascript) of it's own page. * @param response the header response. * @return true if the action (was completed successfully and) should be removed from the action buffer. If this action cannot be executed on a header response returns false. */ boolean apply(IHeaderResponse response); } /** * Page action that just needs to render some javascript. * It can perform the action on the onLoad of a page as well, not just by ajax request target. */ public static class JSChangeAction implements PageAction { private final String js; public JSChangeAction(String js) { this.js = js; } public boolean apply(AjaxRequestTarget target, String childFrameBatchId) { boolean applied = false; if (childFrameBatchId == null) { applied = true; target.appendJavascript(js); // only if req. comes from current page } return applied; } public boolean apply(IHeaderResponse response) { response.renderOnLoadJavascript(js); return true; } } public static class RenderComponentAction implements PageAction { private final Component component; public RenderComponentAction(Component component) { this.component = component; } @Override public boolean apply(AjaxRequestTarget target, String childFrameBatchId) { target.addComponent(component); return true; } @Override public boolean apply(IHeaderResponse response) { return true; } } public static class DivDialogAction implements PageAction { public static final int OP_SHOW = 1; public static final int OP_CLOSE = 2; public static final int OP_DIALOG_ADDED_OR_REMOVED = 3; public static final int OP_TO_FRONT = 4; public static final int OP_TO_BACK = 5; public static final int OP_SET_BOUNDS = 6; public static final int OP_SAVE_BOUNDS = 7; public static final int OP_RESET_BOUNDS = 8; private static final int OP_REATTACH_BEHAVIORS_ON_CORRECT_PAGE = 101; private final ServoyDivDialog divDialog; private final int operation; private final Object[] parameters; public DivDialogAction(ServoyDivDialog divDialog, int operation) { this(divDialog, operation, null); } public DivDialogAction(ServoyDivDialog divDialog, int operation, Object[] parameters) { this.divDialog = divDialog; this.operation = operation; this.parameters = parameters; } public boolean apply(AjaxRequestTarget target, String childFrameBatchId) { // pageName == null if this is executed boolean applied = true; switch (operation) { case DivDialogAction.OP_SHOW : if (!divDialog.isShown()) { divDialog.setPageMapName((String)parameters[0]); divDialog.show(target, childFrameBatchId); // if show is called from a child iFrame request, all callback scripts will point to incorrect page; // we need to schedule a reattach for them from the main/parent iframe. if (childFrameBatchId != null) { ((MainPage)divDialog.getPage()).addJSAction(new DivDialogAction(divDialog, DivDialogAction.OP_REATTACH_BEHAVIORS_ON_CORRECT_PAGE, new Object[] { childFrameBatchId })); } } break; case DivDialogAction.OP_REATTACH_BEHAVIORS_ON_CORRECT_PAGE : applied = (childFrameBatchId == null); if (applied && divDialog.isShown()) { divDialog.reAttachBehaviorsAfterShow(target, (String)parameters[0]); } break; case DivDialogAction.OP_CLOSE : if (divDialog.isShown()) { divDialog.close(target, childFrameBatchId); } break; case DivDialogAction.OP_TO_FRONT : if (divDialog.getPageMapName() != null && divDialog.isShown()) { divDialog.toFront(target, childFrameBatchId); } break; case DivDialogAction.OP_TO_BACK : if (divDialog.getPageMapName() != null && divDialog.isShown()) { divDialog.toBack(target, childFrameBatchId); } break; case DivDialogAction.OP_DIALOG_ADDED_OR_REMOVED : // this should only happen when the request comes from the root iframe itself, otherwise it has no effect (the component cannot be rendered on a response from another page); // even though show is permitted to be called from another (iframe) page's response, the behaviors defined on this component should still work serverside when called from JS, even though it's // not yet rendered client side. applied = (childFrameBatchId == null); if (applied) target.addComponent((WebMarkupContainer)parameters[0]); break; case DivDialogAction.OP_SET_BOUNDS : divDialog.setBounds(target, (Integer)(parameters[0]), (Integer)parameters[1], (Integer)parameters[2], (Integer)parameters[3], childFrameBatchId); break; case DivDialogAction.OP_SAVE_BOUNDS : divDialog.saveBounds(target, childFrameBatchId); break; case DivDialogAction.OP_RESET_BOUNDS : DivWindow.deleteStoredBounds(target, (String)parameters[0]); } onAfterApply(); return applied; } /** * Does nothing; gets called after apply executes. * Can be overriden. */ protected void onAfterApply() { // can be overridden to do stuff after apply } public boolean apply(IHeaderResponse response) { return false; } @Override public String toString() { String strOperation = "OP_SHOW"; switch (operation) { case DivDialogAction.OP_REATTACH_BEHAVIORS_ON_CORRECT_PAGE : strOperation = "OP_REATTACH_BEHAVIORS_ON_CORRECT_PAGE"; break; case DivDialogAction.OP_CLOSE : strOperation = "OP_CLOSE"; break; case DivDialogAction.OP_TO_FRONT : strOperation = "OP_TO_FRONT"; break; case DivDialogAction.OP_TO_BACK : strOperation = "OP_TO_BACK"; break; case DivDialogAction.OP_DIALOG_ADDED_OR_REMOVED : strOperation = "OP_DIALOG_ADDED_OR_REMOVED"; break; case DivDialogAction.OP_SET_BOUNDS : strOperation = "OP_SET_BOUNDS"; break; case DivDialogAction.OP_SAVE_BOUNDS : strOperation = "OP_SAVE_BOUNDS"; break; case DivDialogAction.OP_RESET_BOUNDS : strOperation = "OP_RESET_BOUNDS"; } return "DivDialogAction[" + strOperation + "," + divDialog.getPageMapName() + "]"; } } public static class TriggerAjaxUpdateAction extends JSChangeAction { private final MainPage page; public TriggerAjaxUpdateAction(MainPage page, String triggerScript) { super(triggerScript); this.page = page; } public MainPage getPage() { return page; } } public synchronized void addAction(PageAction a) { buffer1.add(a); } public void apply(AjaxRequestTarget target) { apply(target, null); } public synchronized void apply(AjaxRequestTarget target, PageJSActionBuffer toBeAppliedAsWell) { Iterator<PageAction> it = buffer1.iterator(); while (it.hasNext()) { if (it.next().apply(target, null)) it.remove(); } Iterator<TriggerAjaxUpdateAction> it1 = buffer2.iterator(); while (it1.hasNext()) { if (it1.next().apply(target, null)) it1.remove(); } // what follows here is possibly running div window operations that are queued in the root iframe; // do this after the triggers above because one of the following operations might be a close on the current request's page and we // want to limit the chance that js is still trying to execute in a disposed page if (toBeAppliedAsWell != null) { if (toBeAppliedAsWell.buffer1.size() > 0) { String bID = newBatchID(); DivWindow.beginActionBatch(target, bID); try { // not using iterators in here cause divWindow show action can add an item // to this buffer while iterating (reAttachBehaviors) for (int i = 0; i < toBeAppliedAsWell.buffer1.size();) { if (toBeAppliedAsWell.buffer1.get(i).apply(target, bID)) { toBeAppliedAsWell.buffer1.remove(i); } else i++; } } finally { DivWindow.actionBatchComplete(target, bID); } } } } /** * @return true if all scheduled actions were applied. False if more actions remain to be executed. */ public synchronized boolean apply(IHeaderResponse headerResponse) { Iterator<PageAction> it = buffer1.iterator(); while (it.hasNext()) { if (it.next().apply(headerResponse)) it.remove(); } Iterator<TriggerAjaxUpdateAction> it1 = buffer2.iterator(); while (it1.hasNext()) { if (it1.next().apply(headerResponse)) it1.remove(); } return (buffer1.size() == 0) && (buffer2.size() == 0); } public synchronized void clear() { buffer1.clear(); buffer2.clear(); } public synchronized void triggerAjaxUpdate(MainPage mainPage, String triggerScript) { if (!hasAjaxUpdateTrigger(mainPage)) buffer2.add(new TriggerAjaxUpdateAction(mainPage, triggerScript)); } public synchronized boolean hasAjaxUpdateTrigger(MainPage mainPage) { boolean alreadyThere = false; for (TriggerAjaxUpdateAction a : buffer2) { if (a.getPage() == mainPage) { alreadyThere = true; break; } } return alreadyThere; } /** * returns the buffer that is used in the {@link #addAction(PageAction)} */ public List<PageAction> getBuffer() { return buffer1; } }