001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.ajax;
018
019import java.util.UUID;
020
021import org.apache.wicket.Component;
022import org.apache.wicket.Page;
023import org.apache.wicket.ajax.attributes.AjaxCallListener;
024import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
025import org.apache.wicket.markup.head.IHeaderResponse;
026import org.apache.wicket.markup.head.OnLoadHeaderItem;
027import org.apache.wicket.util.lang.Args;
028import org.apache.wicket.util.string.Strings;
029import org.danekja.java.util.function.serializable.SerializableConsumer;
030
031/**
032 * An Ajax behavior that notifies when a new browser window/tab is opened with url to a page
033 * instance which is already opened in another window/tab in the same user session.
034 *
035 * @since 6.0
036 * @see #onNewWindow(AjaxRequestTarget)
037 */
038public class AjaxNewWindowNotifyingBehavior extends AbstractDefaultAjaxBehavior
039{
040        private static final long serialVersionUID = 1L;
041
042        /**
043         * The name of the HTTP request parameter that transports the current page window's name.
044         */
045        private static final String PARAM_WINDOW_NAME = "windowName";
046
047        /**
048         * The name of the window the page is bound to.
049         */
050        private String boundName;
051
052        /**
053         * Returns the window's name.
054         * 
055         * @return name of {@code null} if not yet bound to a window
056         */
057        public String getWindowName()
058        {
059                return boundName;
060        }
061        
062        /**
063         * Overridden to add the current window name to the request.
064         */
065        @Override
066        protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
067        {
068                super.updateAjaxAttributes(attributes);
069
070                String parameter = "return {'" + PARAM_WINDOW_NAME + "': window.name}";
071                attributes.getDynamicExtraParameters().add(parameter);
072
073                if (boundName != null)
074                {
075                        // already bound, send request only when changed
076                        attributes.getAjaxCallListeners().add(new AjaxCallListener()
077                        {
078                                @Override
079                                public CharSequence getPrecondition(Component component)
080                                {
081                                        return String.format("return (window.name !== '%s');", boundName);
082                                }
083                        });
084                }
085        }
086
087        /**
088         * Overridden to initiate a request once the page was rendered.
089         */
090        @Override
091        public void renderHead(Component component, IHeaderResponse response)
092        {
093                super.renderHead(component, response);
094
095                response.render(OnLoadHeaderItem
096                        .forScript("setTimeout(function() {" + getCallbackScript().toString() + "}, 30);"));
097        }
098
099        @Override
100        protected void respond(AjaxRequestTarget target)
101        {
102                String windowName = getComponent().getRequest().getRequestParameters().getParameterValue(PARAM_WINDOW_NAME).toString();
103
104                if (boundName == null)
105                {
106                        // not bound to any window yet
107
108                        if (Strings.isEmpty(windowName))
109                        {
110                                // create new name
111                                windowName = newWindowName();
112                                target.appendJavaScript(String.format("window.name = '%s';", windowName));
113                        }
114
115                        // now bound to window
116                        boundName = windowName;
117                }
118                else if (boundName.equals(windowName) == false)
119                {
120                        onNewWindow(target);
121                }
122        }
123
124        /**
125         * Create a name for a nameless window, default uses a random {@link UUID}.
126         * 
127         * @return window name
128         */
129        protected String newWindowName()
130        {
131                return UUID.randomUUID().toString();
132        }
133
134        /**
135         * A callback method that is called when a new window/tab is opened for a page instance
136         * which is already opened in another window/tab.
137         * <p>
138         * Default implementation redirects to a new page instance with identical page parameters.
139         *
140         * @param target
141         *            the current request handler
142         */
143        protected void onNewWindow(AjaxRequestTarget target)
144        {
145                Page page = getComponent().getPage();
146
147                getComponent().setResponsePage(page.getClass(), page.getPageParameters());
148        }
149
150        /**
151         * Creates an {@link AjaxNewWindowNotifyingBehavior} based on lambda expressions
152         * 
153         * @param onNewWindow
154         *            the {@code SerializableConsumer} which accepts the {@link AjaxRequestTarget}
155         * @return the {@link AjaxNewWindowNotifyingBehavior}
156         */
157        public static AjaxNewWindowNotifyingBehavior onNewWindow(SerializableConsumer<AjaxRequestTarget> onNewWindow)
158        {
159                Args.notNull(onNewWindow, "onNewWindow");
160
161                return new AjaxNewWindowNotifyingBehavior()
162                {
163                        private static final long serialVersionUID = 1L;
164
165                        @Override
166                        protected void onNewWindow(AjaxRequestTarget target)
167                        {
168                                onNewWindow.accept(target);
169                        }
170                };
171        }
172}