blob: 60eb8bfebcaf131001d5bc0bbbd25db383b6a038 [file] [log] [blame]
/* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
/*
* This source code implements specifications defined by the Java
* Community Process. In order to remain compliant with the specification
* DO NOT add / change / or delete method signatures!
*/
/**
* @fileOverview
* This module provides the implementation for the Pluto portlet hub.
* <p>
*
* @author Scott Nicklous
* @copyright IBM Corp., 2014
*/
var portlet = portlet || {};
(function () {
'use strict';
var isInitialized = false,
/**
* The object holding the portlet data for each portlet on the page along with
* mapping information for public render paramters.
* The portlet data is indexed by portlet ID.
* @property {PortletData} {string}
* The object holds one portlet data object for each portlet. The string
* property name is the portlet ID.
* @property {prpMap} {"<groupIndex>" : {"pid" : "<prpName>"}}
* @private
*/
pageState = {},
/**
* Flag specifying whether history is to be processed (true if browser supports HTML5 session history APIs)
* @property {boolean} doHistory
* @private
*/
doHistory = (window.history && window.history.pushState),
/**
* Callback function provided to the portlet hub to allow the the implementation
* to initiate an unsolicited portlet state update for an array of portlet IDs.
* @param {string[]} pid An array of portlet IDs
* @returns {void}
* @function
* @private
*/
updateWhenIdle,
/**
* Determines if the specified portlet ID is present.
* @param {string} pid The portlet ID
* @returns {boolean} true if the portlet ID can be found
* @function
* @private
*/
isValidId = function (pid) {
var id;
for (id in pageState.portlets) {
if (pageState.portlets.hasOwnProperty(id)) {
if (pid === id) {
return true;
}
}
}
return false;
},
/**
* get the available portlet IDs in an array
*/
getIds = function () {
var id, ids = [];
for (id in pageState.portlets) {
if (pageState.portlets.hasOwnProperty(id)) {
ids.push(id);
}
}
return ids;
},
/**
* gets parameter value
*/
getParmVal = function (pid, name) {
return pageState.portlets[pid].state.parameters[name];
},
/**
* Compares the values of two parameters and returns true if they are equal
*
* @param {string[]} parm1 First parameter
* @param {string[]} parm2 2nd parameter
* @returns {boolean} true if the new parm value is equal to the current value
* @private
*/
_isParmEqual = function(parm1, parm2) {
var ii;
// The values are either string arrays or undefined.
if ((parm1 === undefined) && (parm2 === undefined)) {
return true;
}
if ((parm1 === undefined) || (parm2 === undefined)) {
return false;
}
if (parm1.length !== parm2.length) {
return false;
}
for (ii = parm1.length - 1; ii >= 0; ii--) {
if (parm1[ii] !== parm2[ii]) {
return false;
}
}
return true;
},
/**
* Returns true if input state differs from the current page state.
* Throws exception if input state is malformed.
*/
stateChanged = function (nstate, pid) {
var ostate, pname, nparm, oparm, result = false;
ostate = pageState.portlets[pid].state;
if (!nstate.portletMode || !nstate.windowState || !nstate.parameters) {
throw new Error ("Error decoding state: " + nstate);
}
if (nstate.portletMode !== ostate.portletMode) {
result = true;
} else {
if (nstate.windowState !== ostate.windowState) {
result = true;
} else {
// Has a parameter changed or been added?
for (pname in nstate.parameters) {
if (nstate.parameters.hasOwnProperty(pname)) {
nparm = nstate.parameters[pname];
oparm = ostate.parameters[pname];
if (!_isParmEqual(nparm, oparm)) {
result = true;
}
}
}
// make sure no parameter was deleted
for (pname in ostate.parameters) {
if (ostate.parameters.hasOwnProperty(pname)) {
if (!nstate.parameters[pname]) {
result = true;
}
}
}
}
}
return result;
},
/**
* Compares the values of the named parameter in the new portlet state
* with the values of that parameter in the current state.
*
* @param {string} pid The portlet ID
* @param {PortletState} state The new portlet state
* @param {string} name The parameter name to check
* @returns {boolean} true if the new parm value is different
* from the current value
* @private
*/
isParmInStateEqual = function (pid, state, name) {
var newVal = state.parameters[name], oldVal = getParmVal(pid, name);
return _isParmEqual(newVal, oldVal);
},
/**
* gets defeined PRPs for the portlet
*/
getPRPNames = function (pid) {
return pageState.portlets[pid].pubParms;
},
/**
* function for checking if the parameter is public
*/
isPRP = function (pid, name) {
var prp, result = false, prps = pageState.portlets[pid].pubParms;
for (prp in prps) {
if (prps.hasOwnProperty(prp)) {
if (name === prp) {
result = true;
}
}
}
return result;
},
/**
* Gets the updated public parameters for the given portlet
* ID and new portlet state.
* Returns an object whose properties are the gruop indexes of the
* updated public parameters. The values are the new public
* parameter values.
*
* @param {string} pid The portlet ID
* @param {PortletState} state The new portlet state
* @returns {object} object containing the updated PRPs
* @private
*/
getUpdatedPRPs = function (pid, state) {
var prps = {}, prpNames = getPRPNames(pid), name, group;
for (name in prpNames) {
if (prpNames.hasOwnProperty(name)) {
if (isParmInStateEqual(pid, state, name) === false) {
group = prpNames[name];
prps[group] = state.parameters[name];
}
}
}
return prps;
},
/**
* Returns a deep-copy clone of the input portlet state object.
* Used to provide the portlet client with a copy of the current
* state data rather than a reference to the live state itself.
*
* @param {PortletState} state The portlet state object to check
* @returns {PortletState} Clone of the input portlet state
* @private
*/
cloneState = function (aState) {
var newParams = {},
newState = {
portletMode : aState.portletMode,
windowState : aState.windowState,
parameters : newParams
}, key, oldParams = aState.parameters;
for (key in oldParams) {
if (oldParams.hasOwnProperty(key)) {
newParams[key] = oldParams[key].slice(0);
}
}
return newState;
},
/**
* Get allowed window states for portlet
*/
getAllowedWS = function (pid) {
return pageState.portlets[pid].allowedWS.slice(0);
},
/**
* Get allowed portlet modes for portlet
*/
getAllowedPM = function (pid) {
return pageState.portlets[pid].allowedPM.slice(0);
},
/**
* gets render data for the portlet
*/
getRenderData = function (pid) {
return pageState.portlets[pid].renderData;
},
/**
* gets state for the portlet
*/
getState = function (pid) {
return pageState.portlets[pid].state;
},
/**
* Function to encode url tokens as Pluto likes it
*/
plutoEncode = function (istr) {
var ENCODINGS = {
"_" : "0x1",
"." : "0x2",
"/" : "0x3",
"\r" : "0x4",
"\n" : "0x5",
"<" : "0x6",
">" : "0x7",
" " : "0x8",
"#" : "0x9",
"?" : "0xa",
"\\" : "0xb",
"%" : "0xc",
"|" : "%7C"
}, ii, ostr = "";
for (ii=0; ii < istr.length; ii++) {
if (ENCODINGS[istr.charAt(ii)]) {
ostr += ENCODINGS[istr.charAt(ii)];
} else {
ostr += istr.charAt(ii);
}
}
return ostr;
},
// Constants used for URL Encoding/Decoding (copied from Pluto impl code) ----------
PREFIX = "__",
DELIM = "_",
PORTLET_ID = "pd",
ACTION = "ac",
RESOURCE = "rs",
RESOURCE_ID = "ri",
CACHE_LEVEL = "cl",
RENDER_PARAM = "rp",
PRIVATE_RENDER_PARAM = "pr",
PUBLIC_RENDER_PARAM = "sp",
WINDOW_STATE = "ws",
PORTLET_MODE = "pm",
VALUE_DELIM = "0x0",
AJAX_ACTION = "aa", // new for portlet spec 3
PARTIAL_ACTION = "pa", // new for portlet spec 3
pidMap = {},
/**
* Set up the portlet ID mapping table for URL generation
*/
getPidMap = function () {
var pid, pids = getIds(), ii, urlmap = "";
pidMap = {};
for (ii = 0; ii < pids.length; ii++) {
pid = pids[ii];
pidMap[pid] = ii;
urlmap += "/" + PREFIX + PORTLET_ID;
urlmap += plutoEncode(pageState.portlets[pid].urlpid);
urlmap += DELIM + ii;
}
return urlmap;
},
/**
* Helper for generating parameter strings for the URL
*/
genParmString = function (pid, name, type, group) {
var vals, jj, sep, str = "", wid = "", grpstr = "";
vals = pageState.portlets[pid].state.parameters[name];
// If encoding a render parameter, insert the pid in Pluto internal form
// as opposed to namespace form -
if (type === RENDER_PARAM || type === PUBLIC_RENDER_PARAM) {
wid = pidMap[pid];
}
if (type === PUBLIC_RENDER_PARAM) {
grpstr = DELIM + plutoEncode(group);
}
// If values are present, encode the multivalued parameter string
if (vals) {
sep = VALUE_DELIM;
str += "/" + PREFIX + type + wid + grpstr + DELIM + encodeURIComponent(name);
for (jj=0; jj < vals.length; jj++) {
str += sep + encodeURIComponent(vals[jj]);
}
}
return str;
},
/**
* Helper for generating portlet mode & window state strings for the URL
*/
genPMWSString = function (pid) {
var pm = pageState.portlets[pid].state.portletMode,
ws = pageState.portlets[pid].state.windowState,
wid = pidMap[pid], str = "";
str += "/" + PREFIX + PORTLET_MODE + wid + DELIM + encodeURIComponent(pm);
str += "/" + PREFIX + WINDOW_STATE + wid + DELIM + encodeURIComponent(ws);
return str;
},
/**
* Returns a URL of the specified type.
*
* @param {string} type The URL type
* @param {string} pid The portlet ID
* @param {PortletParameters} parms
* Additional parameters. May be <code>null</code>
* @param {string} cache Cacheability. Must be present if
* type = "RESOURCE". May be <code>null</code>
* @private
*/
getUrl = function (type, pid, parms, cache) {
var url = portlet.impl.getUrlBase(), ca = 'cacheLevelPage', parm, isAction = false,
sep = "", name, names, vals, ii, str, id, ids, tpid, prpstrings, group;
url += getPidMap();
// If target portlet not defined for render URL, set it to null
if ((type === "RENDER") && pid === undefined) {
pid = null;
}
// First add the appropriate window identifier according to URL type.
// Note that no special window ID is added to a RENDER URL.
if (type === "RESOURCE") {
// If generating resource URL, add resource window & cacheability
url += "/" + PREFIX + RESOURCE + pidMap[pid];
if (cache) {
ca = cache;
}
url += "/" + PREFIX + CACHE_LEVEL + plutoEncode(ca);
} else if (type === "ACTION") {
// Add Ajax Action window
isAction = true;
url += "/" + PREFIX + AJAX_ACTION + pidMap[pid];
} else if (type === "PARTIAL_ACTION") {
// Add Partial Action window
isAction = true;
url += "/" + PREFIX + PARTIAL_ACTION + pidMap[pid];
}
// Now add the state to the URL, taking into account cacheability if
// we're dealing with a resource URL.
// Put the private & public parameters on the URL if cacheability != FULL
if ((type !== "RESOURCE") || (ca !== "cacheLevelFull")) {
// If cacheability = PAGE, add the state for the non-target portlets
if ((type !== "RESOURCE") || (ca === "cacheLevelPage")) {
ids = getIds();
for (ii = 0; ii < ids.length; ii++) {
id = ids[ii];
if (id !== pid) { // only for non-target portlets
url += genPMWSString(id); // portlet mode & window state
str = "";
names = pageState.portlets[id].state.parameters;
for (name in names) {
// Public render parameters are encoded separately
if (names.hasOwnProperty(name) && !isPRP(id, name)) {
str += genParmString(id, name, RENDER_PARAM);
}
}
url += str;
}
}
}
// add the state for the target portlet for on-action urls.
// (Action URLs have only action parameters in the query string)
// (for a render URL, pid can be null)
if (!isAction && pid !== null) {
url += genPMWSString(pid); // portlet mode & window state
str = "";
names = pageState.portlets[pid].state.parameters;
for (name in names) {
// Public render parameters are encoded separately
if (names.hasOwnProperty(name) && !isPRP(pid, name)) {
str += genParmString(pid, name, PRIVATE_RENDER_PARAM);
}
}
url += str;
}
// Add the public render parameters for all portlets
str = "";
prpstrings = {};
for (group in pageState.prpMap) {
if (pageState.prpMap.hasOwnProperty(group)) {
for (tpid in pageState.prpMap[group]) {
if (pageState.prpMap[group].hasOwnProperty(tpid)) {
name = pageState.prpMap[group][tpid];
// only need to add parameter once, since it is shared
if (!prpstrings.hasOwnProperty(group)) {
prpstrings[group] = genParmString(tpid, name, PUBLIC_RENDER_PARAM, group);
str += prpstrings[group];
}
}
}
}
}
url += str;
}
// Encode resource or action parameters as query string
if (parms) {
str = ""; sep = "?";
for (parm in parms) {
if (parms.hasOwnProperty(parm)) {
vals = parms[parm];
for (ii=0; ii < vals.length; ii++) {
str += sep + encodeURIComponent(parm) + "=" + encodeURIComponent(vals[ii]);
sep = "&";
}
}
}
url += str;
}
// Use Promise to allow for potential server communication -
return new Promise(function (resolve) {
resolve(url);
});
},
/**
* Called when the page state has been updated to allow the
* browser history to be taken care of.
* @param {boolean} replace replace the state rather than pushing
*/
updateHistory = function (replace) {
if (doHistory) {
getUrl('RENDER', null, {}).then(function (url) {
var token = JSON.stringify(pageState);
console.log("Updating history. URL =" + url + ", token length =" + token.length
+ ", token 30 chars =" + token.substring(0,30));
if (replace) {
history.replaceState(token, "");
} else {
history.pushState(token, "", url);
}
});
}
},
/**
* sets state for the portlet. returns
* array of IDs for portlets that were affected by the change,
* taking into account the public render parameters.
*/
setState = function (pid, state) {
var prps = getUpdatedPRPs(pid, state), group, tpid, upids = [], newVal, prpName, groupMap;
// For each updated PRP group for the
// initiating portlet, update that PRP in the other portlets.
for (group in prps) {
if (prps.hasOwnProperty(group)) {
newVal = prps[group];
// access the PRP map to get the affected portlets
groupMap = pageState.prpMap[group];
for (tpid in groupMap) {
if (groupMap.hasOwnProperty(tpid) && (tpid !== pid)) {
prpName = groupMap[tpid];
if (newVal === undefined) {
delete pageState.portlets[tpid].state.parameters[prpName];
} else {
pageState.portlets[tpid].state.parameters[prpName] = newVal.slice(0);
}
upids.push(tpid);
}
}
}
}
// update state for the initiating portlet
pageState.portlets[pid].state = state;
upids.push(pid);
updateHistory();
// Use Promise to allow for potential server communication -
return new Promise(function (resolve, reject) {
var simval = '';
if (pid === 'SimulateCommError' && state.parameters.SimulateError !== undefined) {
simval = state.parameters.SimulateError[0];
}
// reject promise of an error is to be simulated
if (simval === 'reject') {
reject(new Error("Simulated error occurred when setting state!"));
} else {
resolve(upids);
}
});
},
// decodes the update strings. The update string is
// a JSON object containing the entire page state. This decoder
// returns an object containing the state for portlets whose
// state has changed as compared to the current page state.
decodeUpdateString = function (ustr) {
var states = {}, ostate, nstate, pid, ps, npids = 0, cpids = 0;
console.log("Decoding string: >>" + ustr + "<<");
ps = JSON.parse(ustr);
for (pid in ps.portlets) {
if (ps.portlets.hasOwnProperty(pid)) {
npids++;
nstate = ps.portlets[pid].state;
ostate = pageState.portlets[pid].state;
if (!nstate || !ostate) {
throw new Error ("Invalid update string. ostate=" + ostate + ", nstate=" + nstate);
}
if (stateChanged(nstate, pid)) {
states[pid] = cloneState(nstate);
cpids++;
}
}
}
console.log("decoded state for " + npids + " portlets. # changed = " + cpids);
return states;
},
/**
* updates page state from string and returns array of portlet IDs
* to be updated.
*
* @param {string} ustr The
* @param {string} pid The portlet ID
* @private
*/
updatePageStateFromString = function (ustr, pid) {
var states, tpid, state, upids = [], stateUpdated = false;
states = decodeUpdateString(ustr);
// Update states and collect IDs of affected portlets.
for (tpid in states) {
if (states.hasOwnProperty(tpid)) {
state = states[tpid];
pageState.portlets[tpid].state = state;
upids.push(tpid);
stateUpdated = true;
}
}
// pid will be null or undefined when called from onpopstate routine.
// In that case, don't update history.
if (stateUpdated && pid) {
updateHistory();
}
return upids;
},
/**
* Update page state passed in after partial action. The list of
* ID's of updated portlets is passed back through a promise in order
* to decouple the layers.
*
* @param {string} ustr The
* @param {string} pid The portlet ID
* @private
*/
updatePageState = function (ustr, pid) {
// Use Promise to allow for potential server communication -
return new Promise(function (resolve, reject) {
var upids;
try {
upids = updatePageStateFromString(ustr, pid);
resolve(upids);
} catch (e) {
reject(new Error("Partial Action Action decode status: " + e.message));
}
});
},
/**
* function to extract data from form and encode it as an 'application/x-www-form-urlencoded' string
*
* @param {HTMLFormElement} form Form to be submitted
* @private
*/
encodeFormAsString = function (form) {
var fstr = "", parm, parms = [], ii, jj, el, name, val, tag, type;
for (ii = 0; ii < form.elements.length; ii++) {
el = form.elements[ii];
name = el.name;
val = el.value;
tag = el.nodeName.toUpperCase();
if (tag === 'INPUT') {
type = el.type.toUpperCase();
} else {
type = "";
}
// we don't support file type with enctype = 'application/x-www-form-urlencoded' (hub checks for this case)
if (name && !el.disabled && (type !== 'FILE')) {
if ((tag === 'SELECT') && (el.multiple === true)) {
// multiple select boxes need to be handled differently
for (jj = 0; jj < el.options.length; jj++) {
if (el.options[jj].checked) {
val = el.options[jj].value;
parm = encodeURIComponent(name) + '=' + encodeURIComponent(val);
parms.push(parm);
}
}
} else {
if (((type !== 'CHECKBOX') && (type !== 'RADIO')) || (el.checked === true)) {
parm = encodeURIComponent(name) + '=' + encodeURIComponent(val);
parms.push(parm);
}
}
}
}
fstr = parms.join('&');
return fstr;
},
/**
* performs the actual action.
*
* @param {string} pid The portlet ID
* @param {PortletParameters} parms
* Additional parameters. May be <code>null</code>
* @param {HTMLFormElement} Form to be submitted
* May be <code>null</code>
* @private
*/
executeAction = function (pid, parms, element) {
console.log("impl: executing action. parms=" + parms + ", element=" + element);
// create & return promise to caller.
return new Promise(function (resolve, reject) {
// get the ajax action URL. The Pluto impl creates the URL in JS
// therefore no error handling
getUrl("ACTION", pid, parms).then(function (url) {
var xhr, upids, fd, method = 'POST', enctype, fstr;
console.log("ajax action URL: " + url);
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
upids = updatePageStateFromString(xhr.responseText, pid);
resolve(upids);
} catch (e) {
reject(new Error("Ajax Action decode status: " + e.message));
}
} else {
reject(new Error("Ajax Action xhr status: " + xhr.statusText));
}
}
};
if (element) {
enctype = element.enctype;
if (enctype === 'multipart\/form-data') {
// multipart/form-data is always POSTed using FormData
fd = new FormData(element);
console.log("ajax action: POST using FormData object: " + fd);
xhr.open(method, url, true);
xhr.send(fd);
} else {
// has to be 'application\/x-www-form-urlencoded', as the hub does not support text/plain
method = element.method ? element.method.toUpperCase() : 'GET'; // may be GET or POST; GET is default
fstr = encodeFormAsString(element);
console.log("ajax action: " + method + " using urlencoded form data: " + fstr);
if (method === 'GET') {
// send form data as part of URL
if (url.indexOf('?') >= 0) {
url += '&' + fstr;
} else {
url += '?' + fstr;
}
xhr.open(method, url, true);
xhr.send();
} else {
// has to be post, since we only support GET & POST
xhr.open(method, url, true);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.setRequestHeader('Content-Length', fstr.length);
xhr.send(fstr);
}
}
} else {
xhr.open(method, url, true);
console.log("ajax action: POST using URL with parameters");
xhr.send();
}
});
});
};
/**
* Handler for history event that is fired when the back button is pressed.
*/
if (doHistory) {
window.addEventListener('popstate', function (ev) {
if (ev.state) {
console.log("onpopstate fired. State = " + ev.state.substr(0, 30) + " ...<more>");
updateWhenIdle().then(function (doUpdate) {
var upids = updatePageStateFromString(ev.state);
doUpdate(upids);
});
}
});
}
/**
* Register a portlet. The impl is passed the portlet ID for the portlet.
* The impl must retrieve the information for the portlet in an appropriate
* manner. It must return a Promise that is fulfilled when data for the
* portlet becomes available and is rejected if an error occurs or if the
* portlet ID is invalid.
*
* @param {string} pid Portlet ID
*
* @returns {Promise} fulfilled when data is available
*
* @function
* @private
*/
portlet.impl = portlet.impl || {};
portlet.impl.register = function (pid, updateFunction) {
// take care of moc data initialization
if (!isInitialized) {
pageState = portlet.impl.getInitData();
updateHistory(true);
isInitialized = true;
}
updateWhenIdle = updateFunction;
// stubs for accessing data for this portlet
var stubs = {
/**
* Get allowed window states for portlet
*/
getAllowedWS : function () {return getAllowedWS(pid);},
/**
* Get allowed portlet modes for portlet
*/
getAllowedPM : function () {return getAllowedPM(pid);},
/**
* Get render data for portlet, if any
*/
getRenderData : function () {return getRenderData(pid);},
/**
* Get current portlet state
*/
getState : function () {return getState(pid);},
/**
* Set new portlet state. Returns promise fullfilled with an array of
* IDs of portlets whose state have been modified.
*/
setState : function (state) {return setState(pid, state);},
/**
* Perform the Ajax action request
*/
executeAction : function (parms, element) {return executeAction(pid, parms, element);},
/**
* Get a URL of the specified type - resource or partial action
*/
getUrl : function (type, parms, cache) {return getUrl(type, pid, parms, cache);},
/**
* Decode the update string returned by the partial action request.
* Returns array of IDs of portlets to be updated.
*/
decodeUpdateString : function (ustr) {return updatePageState(ustr, pid);}
};
return new Promise(
function(resolve, reject) {
// verify the input pid
if (isValidId(pid)) {
switch(pid) {
case 'SimulateLongWait':
window.setTimeout(function () {resolve(stubs);}, 500);
break;
case 'SimulateError':
window.setTimeout(function () {reject(new Error("Bad error happened!"));}, 100);
break;
default:
window.setTimeout(function () {resolve(stubs);}, 10);
}
} else {
reject(new Error("Invalid portlet ID: " + pid));
}
}
);
};
// Expose some internal functions for test purposes -
portlet.test = {
getIds : getIds
};
}());