jsf.js_next_gen
Version:
A next generation typescript reimplementation of jsf.js
428 lines • 22.5 kB
JavaScript
"use strict";
/*! 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.XhrRequest = void 0;
const AsyncRunnable_1 = require("../util/AsyncRunnable");
const mona_dish_1 = require("mona-dish");
const AjaxImpl_1 = require("../AjaxImpl");
const XhrFormData_1 = require("./XhrFormData");
const ErrorData_1 = require("./ErrorData");
const EventData_1 = require("./EventData");
const Lang_1 = require("../util/Lang");
const Const_1 = require("../core/Const");
const RequestDataResolver_1 = require("./RequestDataResolver");
var failSaveExecute = Lang_1.ExtLang.failSaveExecute;
const ExtDomQuery_1 = require("../util/ExtDomQuery");
/**
* Faces XHR Request Wrapper
* as AsyncRunnable for our Asynchronous queue
* This means from the outside the
* xhr request is similar to a Promise in a way
* that you can add then and catch and finally callbacks.
*
*
* The idea is that we basically just enqueue
* a single ajax request into our queue
* and let the queue do the processing.
*
*
*/
class XhrRequest extends AsyncRunnable_1.AsyncRunnable {
/**
* Required Parameters
*
* @param requestContext the request context with all pass through values
* @param internalContext internal context with internal info which is passed through, not used by the user
* Optional Parameters
* @param timeout optional xhr timeout
* @param ajaxType optional request type, default "POST"
* @param contentType optional content type, default "application/x-www-form-urlencoded"
*/
constructor(requestContext, internalContext, timeout = Const_1.NO_TIMEOUT, ajaxType = Const_1.REQ_TYPE_POST, contentType = Const_1.URL_ENCODED) {
super();
this.requestContext = requestContext;
this.internalContext = internalContext;
this.timeout = timeout;
this.ajaxType = ajaxType;
this.contentType = contentType;
this.stopProgress = false;
this.xhrObject = new XMLHttpRequest();
// we omit promises here because we have to deal with cancel functionality,
// and promises to not provide that (yet) instead we have our async queue
// which uses an api internally, which is very close to promises
this.registerXhrCallbacks((data) => this.resolve(data), (data) => this.reject(data));
}
start() {
let ignoreErr = failSaveExecute;
let xhrObject = this.xhrObject;
let sourceForm = mona_dish_1.DQ.byId(this.internalContext.getIf(Const_1.CTX_PARAM_SRC_FRM_ID).value);
let executesArr = () => {
return this.requestContext.getIf(Const_1.CTX_PARAM_REQ_PASS_THR, Const_1.P_EXECUTE).get(Const_1.IDENT_NONE).value.split(/\s+/gi);
};
try {
// encoded we need to decode
// We generated a base representation of the current form
// in case someone has overloaded the viewState with additional decorators we merge
// that in, there is no way around it, the spec allows it and getViewState
// must be called, so whatever getViewState delivers has higher priority then
// whatever the formData object delivers
// the partialIdsArray arr is almost deprecated legacy code where we allowed to send a separate list of partial
// ids for reduced load and server processing, this will be removed soon, we can handle the same via execute
const executes = executesArr();
const partialIdsArray = this.internalContext.getIf(Const_1.CTX_PARAM_PPS).value === true ? executes : [];
const formData = new XhrFormData_1.XhrFormData(sourceForm, (0, RequestDataResolver_1.resoveNamingContainerMapper)(this.internalContext), executes, partialIdsArray);
this.contentType = formData.isMultipartRequest ? "undefined" : this.contentType;
// next step the pass through parameters are merged in for post params
this.requestContext.$nspEnabled = false;
const requestContext = this.requestContext;
const requestPassThroughParams = requestContext.getIf(Const_1.CTX_PARAM_REQ_PASS_THR);
// we are turning off here the jsf, faces remapping because we are now dealing with
// pass-through parameters
requestPassThroughParams.$nspEnabled = false;
// this is an extension where we allow pass through parameters to be sent down additionally
// this can be used and is used in the impl to enrich the post request parameters with additional
// information
try {
formData.shallowMerge(requestPassThroughParams, true, true);
}
finally {
// unfortunately as long as we support
// both namespaces we have to keep manual control
// on the key renaming before doing ops like deep copy
this.requestContext.$nspEnabled = true;
requestPassThroughParams.$nspEnabled = true;
}
this.appendIssuingItem(formData);
this.responseContext = requestPassThroughParams.deepCopy;
// we have to shift the internal passthroughs around to build up our response context
const responseContext = this.responseContext;
responseContext.assign(Const_1.CTX_PARAM_MF_INTERNAL).value = this.internalContext.value;
// per spec the onEvent and onError handlers must be passed through to the response
responseContext.assign(Const_1.ON_EVENT).value = requestContext.getIf(Const_1.ON_EVENT).value;
responseContext.assign(Const_1.ON_ERROR).value = requestContext.getIf(Const_1.ON_ERROR).value;
xhrObject.open(this.ajaxType, (0, RequestDataResolver_1.resolveFinalUrl)(sourceForm, formData, this.ajaxType), true);
// adding timeout
this.timeout ? xhrObject.timeout = this.timeout : null;
// a bug in the xhr stub library prevents the setRequestHeader to be properly executed on fake xhr objects
// normal browsers should resolve this
// tests can quietly fail on this one
if (this.contentType != "undefined") {
ignoreErr(() => xhrObject.setRequestHeader(Const_1.CONTENT_TYPE, `${this.contentType}; charset=utf-8`));
}
ignoreErr(() => xhrObject.setRequestHeader(Const_1.HEAD_FACES_REQ, Const_1.VAL_AJAX));
// probably not needed anymore, will test this
// some webkit based mobile browsers do not follow the w3c spec of
// setting, they accept headers automatically
ignoreErr(() => xhrObject.setRequestHeader(Const_1.REQ_ACCEPT, Const_1.STD_ACCEPT));
this.sendEvent(Const_1.BEGIN);
this.sendRequest(formData);
}
catch (e) {
// this happens usually in a client side condition, hence we have to deal in with it in a client
// side manner
this.handleErrorAndClearQueue(e);
throw e;
}
return this;
}
cancel() {
try {
// this causes onError to be called where the error
// handling takes over
this.xhrObject.abort();
}
catch (e) {
this.handleError(e);
}
}
/**
* attaches the internal event and processing
* callback within the promise to our xhr object
*
* @param resolve
* @param reject
*/
registerXhrCallbacks(resolve, reject) {
var _a, _b;
const xhrObject = this.xhrObject;
xhrObject.onabort = () => {
this.onAbort(resolve, reject);
};
xhrObject.ontimeout = () => {
this.onTimeout(resolve, reject);
};
xhrObject.onload = () => {
this.onResponseReceived(resolve);
};
xhrObject.onloadend = () => {
this.onResponseProcessed(this.xhrObject, resolve);
};
if (xhrObject === null || xhrObject === void 0 ? void 0 : xhrObject.upload) {
//this is an extension so that we can send the upload object of the current
//request before any operation
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_PREINIT)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload);
//now we hook in the upload events
xhrObject.upload.addEventListener("progress", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_ON_PROGRESS)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
xhrObject.upload.addEventListener("load", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_LOAD)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
xhrObject.upload.addEventListener("loadstart", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_LOADSTART)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
xhrObject.upload.addEventListener("loadend", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_LOADEND)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
xhrObject.upload.addEventListener("abort", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_ABORT)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
xhrObject.upload.addEventListener("timeout", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_TIMEOUT)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
xhrObject.upload.addEventListener("error", (event) => {
var _a, _b;
(_b = (_a = this.internalContext.getIf(Const_1.CTX_PARAM_UPLOAD_ERROR)).value) === null || _b === void 0 ? void 0 : _b.call(_a, xhrObject.upload, event);
});
}
xhrObject.onerror = (errorData) => {
// Safari in rare cases triggers an error when cancelling a request internally, or when
// in this case we simply ignore the request and clear up the queue, because
// it is not safe anymore to proceed with the current queue
// This bypasses a Safari issue where it keeps requests hanging after page unload
// and then triggers a cancel error on then instead of just stopping
// and clearing the code
// in a page unload case it is safe to clear the queue
// in the exact safari case any request after this one in the queue is invalid
// because the queue references xhr requests to a page which already is gone!
if (this.isCancelledResponse(this.xhrObject)) {
/*
* this triggers the catch chain and after that finally
*/
this.stopProgress = true;
reject();
return;
}
// error already processed somewhere else
if (this.stopProgress) {
return;
}
this.handleError(errorData);
};
}
isCancelledResponse(currentTarget) {
return (currentTarget === null || currentTarget === void 0 ? void 0 : currentTarget.status) === 0 && // cancelled internally by browser
(currentTarget === null || currentTarget === void 0 ? void 0 : currentTarget.readyState) === 4 &&
(currentTarget === null || currentTarget === void 0 ? void 0 : currentTarget.responseText) === '' &&
(currentTarget === null || currentTarget === void 0 ? void 0 : currentTarget.responseXML) === null;
}
/*
* xhr processing callbacks
*
* Those methods are the callbacks called by
* the xhr object depending on its own state
*/
/**
* client side abort... also here for now we clean the queue
*
* @param resolve
* @param reject
* @private
*/
onAbort(resolve, reject) {
// reject means clear queue, in this case we abort entirely the processing
// does not happen yet, we have to probably rethink this strategy in the future
// when we introduce cancel functionality
this.handleHttpError(reject);
}
/**
* request timeout, this must be handled like a generic server error per spec
* unfortunately, so we have to jump to the next item (we cancelled before)
* @param resolve
* @param reject
* @private
*/
onTimeout(resolve, reject) {
// timeout also means we we probably should clear the queue,
// the state is unsafe for the next requests
this.sendEvent(Const_1.STATE_EVT_TIMEOUT);
this.handleHttpError(resolve);
}
/**
* the response is received and normally is a normal response
* but also can be some kind of error (http code >= 300)
* In any case the response will be resolved either as error or response
* and the next item in the queue will be processed
* @param resolve
* @private
*/
onResponseReceived(resolve) {
var _a;
this.sendEvent(Const_1.COMPLETE);
//request error resolution as per spec:
if (!this.processRequestErrors(resolve)) {
(0, Const_1.$faces)().ajax.response(this.xhrObject, (_a = this.responseContext.value) !== null && _a !== void 0 ? _a : {});
}
}
processRequestErrors(resolve) {
var _a, _b, _c;
const responseXML = new mona_dish_1.XMLQuery((_a = this.xhrObject) === null || _a === void 0 ? void 0 : _a.responseXML);
const responseCode = (_c = (_b = this.xhrObject) === null || _b === void 0 ? void 0 : _b.status) !== null && _c !== void 0 ? _c : -1;
if (responseXML.isXMLParserError()) {
// invalid response
const errorName = "Invalid Response";
const errorMessage = "The response xml is invalid";
this.handleGenericResponseError(errorName, errorMessage, Const_1.MALFORMEDXML, resolve);
return true;
}
else if (responseXML.isAbsent()) {
// empty response
const errorName = "Empty Response";
const errorMessage = "The response has provided no data";
this.handleGenericResponseError(errorName, errorMessage, Const_1.EMPTY_RESPONSE, resolve);
return true;
}
else if (responseCode >= 300 || responseCode < 200) {
// other server errors
// all errors from the server are resolved without interfering in the queue
this.handleHttpError(resolve);
return true;
}
//additional errors are application errors and must be handled within the response
return false;
}
handleGenericResponseError(errorName, errorMessage, responseStatus, resolve) {
var _a, _b, _c, _d;
const errorData = new ErrorData_1.ErrorData(this.internalContext.getIf(Const_1.CTX_PARAM_SRC_CTL_ID).value, errorName, errorMessage, (_b = (_a = this.xhrObject) === null || _a === void 0 ? void 0 : _a.responseText) !== null && _b !== void 0 ? _b : "", (_d = (_c = this.xhrObject) === null || _c === void 0 ? void 0 : _c.responseXML) !== null && _d !== void 0 ? _d : null, this.xhrObject.status, responseStatus);
this.finalizeError(errorData, resolve);
}
handleHttpError(resolveOrReject, errorMessage = "Generic HTTP Serror") {
var _a, _b, _c, _d, _e, _f;
this.stopProgress = true;
const errorData = new ErrorData_1.ErrorData(this.internalContext.getIf(Const_1.CTX_PARAM_SRC_CTL_ID).value, Const_1.HTTP_ERROR, errorMessage, (_b = (_a = this.xhrObject) === null || _a === void 0 ? void 0 : _a.responseText) !== null && _b !== void 0 ? _b : "", (_d = (_c = this.xhrObject) === null || _c === void 0 ? void 0 : _c.responseXML) !== null && _d !== void 0 ? _d : null, (_f = (_e = this.xhrObject) === null || _e === void 0 ? void 0 : _e.status) !== null && _f !== void 0 ? _f : -1, Const_1.HTTP_ERROR);
this.finalizeError(errorData, resolveOrReject);
}
finalizeError(errorData, resolveOrReject) {
try {
this.handleError(errorData, true);
}
finally {
// we issue a resolveOrReject in this case to allow the system to recover
// reject would clean up the queue
// resolve would trigger the next element in the queue to be processed
resolveOrReject(errorData);
this.stopProgress = true;
}
}
/**
* last minute cleanup, the request now either is fully done
* or not by having had a cancel or error event be
* @param data
* @param resolve
* @private
*/
onResponseProcessed(data, resolve) {
// if stop progress true, the cleanup already has been performed
if (this.stopProgress) {
return;
}
/*
* normal case, cleanup == next item if possible
*/
resolve(data);
}
sendRequest(formData) {
const isPost = this.ajaxType != Const_1.REQ_TYPE_GET;
if (formData.isMultipartRequest) {
// in case of a multipart request we send in a formData object as body
this.xhrObject.send((isPost) ? formData.toFormData() : null);
}
else {
// in case of a normal request we send it normally
this.xhrObject.send((isPost) ? formData.toString() : null);
}
}
/*
* other helpers
*/
sendEvent(evtType) {
var _a;
const eventData = EventData_1.EventData.createFromRequest(this.xhrObject, this.internalContext, this.requestContext, evtType);
try {
// User code error, we might cover
// this in onError, but also we cannot swallow it.
// We need to resolve the local handlers lazily,
// because some frameworks might decorate them over the context in the response
let eventHandler = (0, RequestDataResolver_1.resolveHandlerFunc)(this.requestContext, this.responseContext, Const_1.ON_EVENT);
AjaxImpl_1.Implementation.sendEvent(eventData, eventHandler);
}
catch (e) {
e.source = (_a = e === null || e === void 0 ? void 0 : e.source) !== null && _a !== void 0 ? _a : this.requestContext.getIf(Const_1.SOURCE).value;
// this is a client error, no save state anymore for queue processing!
this.handleErrorAndClearQueue(e);
// we forward the error upward like all client side errors
throw e;
}
}
handleErrorAndClearQueue(e, responseFormatError = false) {
this.handleError(e, responseFormatError);
this.reject(e);
}
handleError(exception, responseFormatError = false) {
var _a;
const errorData = (responseFormatError) ? ErrorData_1.ErrorData.fromHttpConnection(exception.source, exception.type, (_a = exception.message) !== null && _a !== void 0 ? _a : Const_1.EMPTY_STR, exception.responseText, exception.responseXML, exception.responseCode, exception.status) : ErrorData_1.ErrorData.fromClient(exception);
const eventHandler = (0, RequestDataResolver_1.resolveHandlerFunc)(this.requestContext, this.responseContext, Const_1.ON_ERROR);
AjaxImpl_1.Implementation.sendError(errorData, eventHandler);
}
appendIssuingItem(formData) {
var _a, _b;
const issuingItemId = this.internalContext.getIf(Const_1.CTX_PARAM_SRC_CTL_ID).value;
//to avoid sideffects with buttons we only can append the issuing item if no behavior event is set
//MYFACES-4679!
const eventType = (_b = (_a = formData.getIf((0, Const_1.$nsp)(Const_1.P_BEHAVIOR_EVENT)).value) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null;
const isBehaviorEvent = (!!eventType) && eventType != 'click';
//not encoded
if (issuingItemId && formData.getIf(issuingItemId).isAbsent() && !isBehaviorEvent) {
const issuingItem = mona_dish_1.DQ.byId(issuingItemId);
const itemValue = issuingItem.inputValue;
const arr = new ExtDomQuery_1.ExtConfig({});
const type = issuingItem.type.orElse("").value.toLowerCase();
//Checkbox and radio only value pass if checked is set, otherwise they should not show
//up at all, and if checked is set, they either can have a value or simply being boolean
if ((type == XhrRequest.TYPE_CHECKBOX || type == XhrRequest.TYPE_RADIO) && !issuingItem.checked) {
return;
}
else if ((type == XhrRequest.TYPE_CHECKBOX || type == XhrRequest.TYPE_RADIO)) {
arr.assign(issuingItemId).value = itemValue.orElse(true).value;
}
else if (itemValue.isPresent()) {
arr.assign(issuingItemId).value = itemValue.value;
}
formData.shallowMerge(arr, true, true);
}
}
}
exports.XhrRequest = XhrRequest;
XhrRequest.TYPE_CHECKBOX = "checkbox";
XhrRequest.TYPE_RADIO = "radio";
//# sourceMappingURL=XhrRequest.js.map