UNPKG

jsf.js_next_gen

Version:

A next generation typescript reimplementation of jsf.js

428 lines 22.5 kB
"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