/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
* <p>
*/
package org.olat.core.gui.control.winmgr;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.olat.admin.sysinfo.manager.SessionStatsManager;
import org.olat.core.CoreSpringFactory;
import org.olat.core.dispatcher.DispatcherModule;
import org.olat.core.dispatcher.impl.StaticMediaDispatcher;
import org.olat.core.dispatcher.mapper.Mapper;
import org.olat.core.dispatcher.mapper.MapperService;
import org.olat.core.dispatcher.mapper.manager.MapperKey;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.UserRequestImpl;
import org.olat.core.gui.Windows;
import org.olat.core.gui.components.CannotReplaceDOMFragmentException;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.Window;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.ChiefController;
import org.olat.core.gui.control.DefaultController;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowBackOffice;
import org.olat.core.gui.control.pushpoll.WindowCommand;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.media.NothingChangedMediaResource;
import org.olat.core.gui.media.StringMediaResource;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.render.URLBuilder;
import org.olat.core.gui.translator.Translator;
import org.olat.core.helpers.Settings;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.id.context.ContextEntry;
import org.olat.core.id.context.HistoryPoint;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
import org.olat.core.util.WebappHelper;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Description:<br> - to be used by the windowmanager only! - this controller
* manages the state of all browser windows on the client side (javascript) and
* communicates changes to the server side (java)
* <P>
* Initial Date: 17.03.2006 <br>
*
* @author Felix Jost
*/
public class AjaxController extends DefaultController {
private static final String VELOCITY_ROOT = Util.getPackageVelocityRoot(AjaxController.class);
private static final OLog log = Tracing.createLoggerFor(AjaxController.class);
private final VelocityContainer myContent;
private final VelocityContainer pollPeriodContent;
private final Panel mainP;
private final Panel pollperiodPanel;
// protected only for performance improvement
protected List<WindowCommand> windowcommands = new ArrayList<WindowCommand>(3);
private final Mapper m, sbm;
private final MapperKey mKey, sbmKey;
private static final int DEFAULT_POLLPERIOD = 5000;//reasonable default value
private int pollperiod = DEFAULT_POLLPERIOD;//reasonable default value
private int pollCount = 0;
private long creationTime = System.currentTimeMillis();
private boolean ajaxEnabled;
private WindowBackOffice wboImpl;
@Autowired
private SessionStatsManager statsManager;
AjaxController(UserRequest ureq, final WindowBackOfficeImpl wboImpl, boolean ajaxEnabled) {
super(null);
this.ajaxEnabled = ajaxEnabled;
this.wboImpl = wboImpl;
pollPeriodContent = new VelocityContainer("jsserverpartpoll", VELOCITY_ROOT + "/pollperiod.html", null, this);
pollPeriodContent.contextPut("pollperiod", new Integer(pollperiod));
myContent = new VelocityContainer("jsserverpart", VELOCITY_ROOT + "/serverpart.html", null, this);
myContent.contextPut("pollperiod", new Integer(pollperiod));
//more debug information: OLAT-3529
if (ajaxEnabled) myContent.contextPut("isAdmin", Boolean.valueOf(ureq.getUserSession().getRoles().isOLATAdmin()));
// create a mapper to not block main traffic when polling (or vica versa)
final Window window = wboImpl.getWindow();
m = new Mapper() {
@Override
public MediaResource handle(String relPath, HttpServletRequest request) {
pollCount++;
statsManager.incrementAuthenticatedPollerClick();
String uriPrefix = DispatcherModule.getLegacyUriPrefix(request);
UserRequest uureq = new UserRequestImpl(uriPrefix, request, null);
boolean reload = false;
Windows ws = Windows.getWindows(uureq);
if(ws != null && wboImpl.getChiefController() != null) {
ChiefController cc = wboImpl.getChiefController();
reload = cc.wishAsyncReload(uureq, false);
}
MediaResource resource;
try {
// check for dirty components now.
wboImpl.fireCycleEvent(Window.BEFORE_INLINE_RENDERING);
Command updateDirtyCom = window.handleDirties();
wboImpl.fireCycleEvent(Window.AFTER_INLINE_RENDERING);
if (updateDirtyCom != null) {
synchronized (windowcommands) { //o_clusterOK by:fj
windowcommands.add(new WindowCommand(wboImpl, updateDirtyCom));
if(reload) {
String timestampID = uureq.getTimestampID();
String reRenderUri = window.buildURIFor(window, timestampID, null);
Command rmrcom = CommandFactory.createParentRedirectTo(reRenderUri);
windowcommands.add(new WindowCommand(wboImpl, rmrcom));
}
}
resource = extractMediaResource(false);
} else {
resource = new NothingChangedMediaResource();
}
} catch (CannotReplaceDOMFragmentException e) {
String timestampID = uureq.getTimestampID();
String reRenderUri = window.buildURIFor(window, timestampID, null);
Command rmrcom = CommandFactory.createParentRedirectTo(reRenderUri);
windowcommands.add(new WindowCommand(wboImpl, rmrcom));
resource = extractMediaResource(false);
}
return resource;
}
};
mKey = CoreSpringFactory.getImpl(MapperService.class).register(ureq.getUserSession(), m);
myContent.contextPut("mapuri", mKey.getUrl());
mainP = new Panel("ajaxMainPanel");
mainP.setContent(myContent);
pollperiodPanel = new Panel("pollperiodP");
pollperiodPanel.setContent(pollPeriodContent);
myContent.put("pollperiodPanel", pollperiodPanel);
setInitialComponent(mainP);
// either turn ajax on or off
setAjaxEnabled(ajaxEnabled);
// The following is for the "standby page"
final Locale flocale = ureq.getLocale();
sbm = new Mapper() {
Translator t = Util.createPackageTranslator(ChiefController.class, flocale);
@Override
public MediaResource handle(String relPath, HttpServletRequest request) {
StringMediaResource smr = new StringMediaResource();
smr.setContentType("text/html;charset=utf-8");
smr.setEncoding("utf-8");
try {
StringOutput slink = new StringOutput(50);
StaticMediaDispatcher.renderStaticURI(slink, null);
//slink now holds static url base like /olat/raw/700/
URLBuilder ubu = new URLBuilder(WebappHelper.getServletContextPath() + DispatcherModule.PATH_AUTHENTICATED, "1", "1");
StringOutput blink = new StringOutput(50);
ubu.buildURI(blink, null, null);
//blink holds the link back to olat like /olat/auth/1%3A1%3A0%3A0%3A0/
String p = FileUtils.load(getClass().getResourceAsStream("_content/standby.html"), WebappHelper.getDefaultCharset());
p = p.replace("_staticLink_", slink.toString());
p = p.replace("_pageTitle_", t.translate("standby.page.title"));
p = p.replace("_pageHeading_", t.translate("standby.page.heading"));
p = p.replace("_Message_", t.translate("standby.page.message"));
p = p.replace("_Button_", t.translate("standby.page.button"));
p = p.replace("_linkTitle_", t.translate("standby.page.button.title"));
p = p.replace("_olatUrl_", blink.toString());
smr.setData(p);
} catch (Exception e) {
smr.setData(e.toString());
};
return smr;
}
};
sbmKey = CoreSpringFactory.getImpl(MapperService.class).register(ureq.getUserSession(), sbm);
myContent.contextPut("sburi", sbmKey.getUrl());
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.components.Component, org.olat.core.gui.control.Event)
*/
@Override
public void event(UserRequest ureq, Component source, Event event) {
//
}
public void pushResource(UserRequest ureq, Writer sb, boolean wrapHTML) throws IOException {
if (wrapHTML) {
// most ajax responses are a lot smaller than 16k
sb.append("<html><head><script type=\"text/javascript\">\n/* <![CDATA[ */\nfunction invoke(){var r=");
pushJSONAndClear(ureq, sb);
sb.append("; ")
.append("if (parent!=self&&parent.window.o_info){")
.append("parent.window.o_ainvoke(r);")
// normal case: ajax result can be delivered into the hidden iframe.
// else: no parent frame or parent frame is not olat -> reasons:
// a) mouse-right-click to open in new tab/window
// b) fast double-click when target causes a 302 and browser's window's document has been updated, but old link was still clickable
// c) ...
// -> in all cases, do not show the json command, but reload the window which contained the link clicked (= window id of url)
.append(" try{ parent.window.o_removeIframe(document.defaultView.frameElement.id); } catch(e) {} ")
.append("} else {")
// inform user that ajax-request cannot be opened in a new window,
// todo felix: maybe send back request to bookmark-launch current url? -> new window?
// we could then come near to what the user probably wanted when he/she opened a link in a new window
.append("this.document.location=\"")
.append(StaticMediaDispatcher.createStaticURIFor("msg/json/en/info.html"))
.append("\";")
.append("}}\n/* ]]> */\n</script></head><body onLoad=\"invoke()\"></body></html>");
} else {
pushJSONAndClear(ureq, sb);
}
}
public void pushJSONAndClear(UserRequest ureq, Writer writer) throws IOException {
synchronized (windowcommands) { //o_clusterOK by:fj
// handle all windowcommands now, create json
writer.append("{\"cmds\":[");
int sum = windowcommands.size();
if (sum > 0) {
// treat commands waiting for the poll
for (int i = 0; i < sum; i++) {
if(i != 0) writer.append(",");
WindowCommand wc = windowcommands.get(i);
pushJSON(wc, writer);
}
}
writer.append("],\"cmdcnt\":").append(Integer.toString(sum));
appendBusinessPathInfos(ureq, writer);
writer.append("}");
windowcommands.clear();
}
}
private void appendBusinessPathInfos(UserRequest ureq, Writer writer) throws IOException {
ChiefController ctrl = wboImpl.getChiefController();
String documentTitle = ctrl == null ? "" : ctrl.getWindowTitle();
writer.append(",\"documentTitle\":").append(JSONObject.quote(documentTitle));
StringBuilder bc = new StringBuilder(128);
HistoryPoint p = ureq.getUserSession().getLastHistoryPoint();
if(p != null && StringHelper.containsNonWhitespace(p.getBusinessPath())) {
List<ContextEntry> ces = p.getEntries();
String uriPrefix = wboImpl.getWindow().getUriPrefix();
bc.append(uriPrefix)
.append(BusinessControlFactory.getInstance().getAsRestPart(ces, true));
writer.append(",\"businessPath\":").append(JSONObject.quote(bc.toString()));
writer.append(",\"historyPointId\":").append(JSONObject.quote(p.getUuid()));
}
}
private void pushJSON(WindowCommand wc, Writer writer) throws IOException {
Command c = wc.getCommand();
String winId = wc.getWindowBackOffice().getWindow().getDispatchID();
try {
writer.append("{\"w\":\"").append(winId)
.append("\",\"cmd\":").append(Integer.toString(c.getCommand()))
.append(",\"cda\":");
c.getSubJSON().write(writer);
c.getSubJSON().toString(2);
writer.append("}");
} catch (JSONException e) {
throw new AssertException("json exception:", e);
}
}
public MediaResource extractMediaResource(boolean wrapHTML) {
JSONObject json = getAndClearJSON(true);
String res;
if (wrapHTML) {
// most ajax responses are a lot smaller than 16k
StringBuilder sb = new StringBuilder(16384);
sb.append("<html><head><script type=\"text/javascript\">\n/* <![CDATA[ */\nfunction invoke(){var r=")
.append(json.toString()).append("; ")
.append("if (parent!=self&&parent.window.o_info) {")
.append(" parent.window.o_ainvoke(r);")
.append(" try{ parent.window.o_removeIframe(document.defaultView.frameElement.id); } catch(e) {} ")
// normal case: ajax result can be delivered into the hidden iframe.
// else: no parent frame or parent frame is not olat -> reasons:
// a) mouse-right-click to open in new tab/window
// b) fast double-click when target causes a 302 and browser's window's document has been updated, but old link was still clickable
// c) ...
// -> in all cases, do not show the json command, but reload the window which contained the link clicked (= window id of url)
.append("} else {")
// inform user that ajax-request cannot be opened in a new window,
// todo felix: maybe send back request to bookmark-launch current url? -> new window?
// we could then come near to what the user probably wanted when he/she opened a link in a new window
.append(" this.document.location=\"").append(StaticMediaDispatcher.createStaticURIFor("msg/json/en/info.html")).append("\";")
.append("}}")
.append("\n/* ]]> */\n</script></head><body onLoad=\"invoke()\"></body></html>");
res = sb.toString();
} else {
res = json.toString();
}
StringMediaResource smr = new StringMediaResource();
smr.setContentType("text/html;charset=utf-8");
smr.setEncoding("utf-8");
smr.setData(res);
return smr;
}
private JSONObject createJSON(WindowCommand wc) {
Command c = wc.getCommand();
WindowBackOffice wbo = wc.getWindowBackOffice();
String winId = wbo.getWindow().getDispatchID();
JSONObject jo = new JSONObject();
try {
jo.put("cmd", c.getCommand());
jo.put("w", winId);
jo.put("cda", c.getSubJSON());
return jo;
} catch (JSONException e) {
throw new AssertException("json exception:", e);
}
}
/**
* @param moreCmds a List of WindowCommand objects
* @return
*/
private JSONObject getAndClearJSON(boolean clear) {
JSONObject root = new JSONObject();
try {
if (Settings.isDebuging()) {
long time = System.currentTimeMillis();
root.put("time", time);
}
synchronized (windowcommands) { //o_clusterOK by:fj
// handle all windowcommands now, create json
int sum = windowcommands.size();
root.put("cmdcnt", sum); // number of commands: 0..n
if (sum > 0) {
JSONArray ja = new JSONArray();
root.put("cmds", ja);
// treat commands waiting for the poll
for (int i = 0; i < sum; i++) {
WindowCommand wc = windowcommands.get(i);
JSONObject jo = createJSON(wc);
ja.put(jo);
}
if(clear) {
windowcommands.clear();
}
}
}
return root;
} catch (JSONException e) {
throw new AssertException("wrong data put into json object", e);
}
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose(boolean)
*/
@Override
protected void doDispose() {
List<MapperKey> mappers = new ArrayList<MapperKey>();
mappers.add(mKey);
mappers.add(sbmKey);
CoreSpringFactory.getImpl(MapperService.class).cleanUp(mappers);
if (ajaxEnabled && pollCount == 0) {
//the controller should be older than 40s otherwise poll may not started yet
if ((System.currentTimeMillis() - creationTime) > 40000) log.warn("Client did not send a single polling request though ajax is enabled!");
}
}
/**
* @param wco
*/
public void sendCommandTo(WindowCommand wco) {
synchronized (windowcommands) { //o_clusterOK by:fj
windowcommands.add(wco);
}
}
/**
* @param enabled
*/
public void setAjaxEnabled(boolean enabled) {
if (enabled) {
mainP.setContent(myContent);
} else {
mainP.setContent(null);
}
}
/**
*
* @param pollperiod time in ms between two polls
*/
public void setPollPeriod(int pollperiod) {
if (pollperiod != this.pollperiod) {
if (pollperiod == -1) pollperiod = DEFAULT_POLLPERIOD;
this.pollperiod = pollperiod;
pollPeriodContent.contextPut("pollperiod", new Integer(pollperiod));
} // else no need to change anything
}
}