/**
* @file Provides the (http) client/connection to the search backend service.
* @version {@link https://github.com/JohT/search-menu-ui/releases/latest latest version}
* @author JohT
* @version ${project.version}
*/
"use strict";
var module = datarestructorInternalCreateIfNotExists(module); // Fallback for vanilla js without modules
function datarestructorInternalCreateIfNotExists(objectToCheck) {
return objectToCheck || {};
}
/**
* Search-Menu Service-Client.
* It provides the (http) client/connection to the search backend service.
* @module searchMenuServiceClient
*/
var searchMenuServiceClient = (module.exports = {}); // Export module for npm...
searchMenuServiceClient.internalCreateIfNotExists = datarestructorInternalCreateIfNotExists;
var xmlHttpRequest = xmlHttpRequest || require("../../src/js/ponyfills/xmlHttpRequestPonyfill"); // supports vanilla js & npm
searchMenuServiceClient.HttpSearchConfig = (function () {
/**
* Configures and builds the {@link module:searchMenuServiceClient.HttpClient}.
* DescribedDataField is the main element of the restructured data and therefore considered "public".
* @constructs HttpSearchConfig
* @alias module:searchMenuServiceClient.HttpSearchConfig
*/
function HttpSearchConfig() {
/**
* HTTP Search Configuration.
* @property {string} searchUrlTemplate URL that is called for every search request. It may include variables in double curly brackets like `{{searchtext}}`.
* @property {string} [searchMethod="POST"] HTTP Method, that is used for every search request.
* @property {string} [searchContentType="application/json"] HTTP MIME-Type of the body, that is used for every search request.
* @property {string} searchBodyTemplate HTTP body template, that is used for every search request. It may include variables in double curly brackets like `{{jsonSearchParameters}}`.
* @property {XMLHttpRequest} [httpRequest=new XMLHttpRequest()] Contains the XMLHttpRequest that is used to handle HTTP requests and responses. Defaults to XMLHttpRequest.
* @property {boolean} [debugMode=false] Adds detailed logging for development and debugging.
*/
this.config = {
searchUrlTemplate: "",
searchMethod: "POST",
searchContentType: "application/json",
searchBodyTemplate: null,
/**
* Resolves variables in the search url template based on the given search parameters object.
* The variable {{jsonSearchParameters}} will be replaced by the JSON of all search parameters.
* @param {Object} searchParameters object properties will be used to replace the variables of the searchUrlTemplate
*/
resolveSearchUrl: function (searchParameters) {
return resolveTemplate(this.searchUrlTemplate, searchParameters, this.debugMode);
},
/**
* Resolves variables in the search body template based on the given search parameters object.
* The variable {{jsonSearchParameters}} will be replaced by the JSON of all search parameters.
* @param {Object} searchParameters object properties will be used to replace the variables of the searchBodyTemplate
*/
resolveSearchBody: function (searchParameters) {
return resolveTemplate(this.searchBodyTemplate, searchParameters, this.debugMode);
},
httpRequest: null,
debugMode: false
};
/**
* Sets the url for the HTTP request for the search.
* It may include variables in double curly brackets like {{searchtext}}.
* @param {String} value
* @return {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.searchUrlTemplate = function (value) {
this.config.searchUrlTemplate = value;
return this;
};
/**
* Sets the HTTP method for the search. Defaults to "POST".
* @param {String} value
* @return {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.searchMethod = function (value) {
this.config.searchMethod = value;
return this;
};
/**
* Sets the HTTP content type of the request body. Defaults to "application/json".
* @param {String} value
* @return {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.searchContentType = function (value) {
this.config.searchContentType = value;
return this;
};
/**
* Sets the HTTP request body template that may contain variables (e.g. {{searchParameters}}) in double curly brackets, or null if there is none.
* @param {String} value
* @return {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.searchBodyTemplate = function (value) {
this.config.searchBodyTemplate = value;
return this;
};
/**
* Sets the HTTP-Request-Object. Defaults to XMLHttpRequest if not set.
* @param {String} value
* @return {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.httpRequest = function (value) {
this.config.httpRequest = value;
return this;
};
/**
* Sets the debug mode, that prints some more info to the console.
* @param {boolean} value
* @return {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.debugMode = function (value) {
this.config.debugMode = value === true;
return this;
};
/**
* Uses the configuration to build the http client that provides the function "search" (parameters: searchParameters, onSuccess callback).
* @returns {module:searchMenuServiceClient.HttpClient}
*/
this.build = function () {
if (!this.config.httpRequest) {
this.config.httpRequest = xmlHttpRequest.getXMLHttpRequest();
}
return new searchMenuServiceClient.HttpClient(this.config);
};
}
/**
* Resolves variables in the template based on the given search parameters object.
* The variable {{jsonSearchParameters}} will be replaced by the JSON of all search parameters.
* @param {String} template contains variables in double curly brackets that should be replaced by the values of the parameterSourceObject.
* @param {Object} parameterSourceObject object properties will be used to replace the variables of the template
* @param {boolean} debugMode enables/disables extended logging for debugging
* @memberof module:searchMenuServiceClient.HttpSearchConfig
* @protected
*/
function resolveTemplate(template, parameterSourceObject, debugMode) {
if (template == null) {
return null;
}
var jsonSearchParameters = JSON.stringify(parameterSourceObject);
var resolvedBody = template;
resolvedBody = resolveVariableInTemplate(resolvedBody, "jsonSearchParameters", jsonSearchParameters);
resolvedBody = resolveVariablesInTemplate(resolvedBody, parameterSourceObject);
if (debugMode) {
console.log("template=" + template);
console.log("{{jsonSearchParameters}}=" + jsonSearchParameters);
console.log("resolved template=" + resolvedBody);
}
return resolvedBody;
}
function resolveVariablesInTemplate(templateString, sourceDataObject) {
var resolvedString = templateString;
forEachFieldsIn(sourceDataObject, function (fieldName, fieldValue) {
resolvedString = resolveVariableInTemplate(resolvedString, fieldName, fieldValue);
});
return resolvedString;
}
function resolveVariableInTemplate(templateString, fieldName, fieldValue) {
//TODO could there be a better compatible solution to replace ALL occurrences instead of creating regular expressions?
var variableReplaceRegExp = new RegExp("\\{\\{" + escapeCharsForRegEx(fieldName) + "\\}\\}", "gm");
return templateString.replace(variableReplaceRegExp, fieldValue);
}
function escapeCharsForRegEx(characters) {
var nonWordCharactersRegEx = new RegExp("([^-\\w])", "gi");
return characters.replace(nonWordCharactersRegEx, "\\$1");
}
function forEachFieldsIn(object, fieldNameAndValueConsumer) {
var fieldNames = Object.keys(object);
var index, fieldName, fieldValue;
for (index = 0; index < fieldNames.length; index += 1) {
fieldName = fieldNames[index];
fieldValue = object[fieldName];
fieldNameAndValueConsumer(fieldName, fieldValue);
}
}
return HttpSearchConfig;
}());
/**
* This function will be called, when search results are available.
* @callback module:searchMenuServiceClient.HttpClient.SearchServiceResultAvailable
* @param {Object} searchResultData already parsed data object containing the result of the search
*/
searchMenuServiceClient.HttpClient = (function () {
/**
* HttpClient.
*
* Contains the "backend-connection" of the search bar. It submits the search query,
* parses the results and informs the callback as soon as these results are available.
* @example new searchMenuServiceClient.HttpSearchConfig()....build();
* @param {module:searchMenuServiceClient.HttpSearchConfig} config
* @constructs HttpClient
* @alias module:searchMenuServiceClient.HttpClient
*/
var instance = function (config) {
/**
* Configuration for the search HTTP requests.
* @type {module:searchMenuServiceClient.HttpSearchConfig}
*/
this.config = config;
/**
* This function will be called to trigger search (calling the search backend).
* @function
* @param {Object} searchParameters object that contains all parameters as properties. It will be converted to JSON.
* @param {module:searchMenuServiceClient.HttpClient.SearchServiceResultAvailable} onSearchResultsAvailable will be called when search results are available.
*/
this.search = createSearchFunction(this.config, this.config.httpRequest);
};
/**
* Creates the search service function that can be bound to the search menu.
* @param {module:searchMenuServiceClient.HttpSearchConfig} config Configuration for the search HTTP requests.
* @param {XMLHttpRequest} httpRequest Takes the HTTP-Request-Object.
* @returns {module:searchMenuServiceClient.SearchService}
* @memberof module:searchMenuServiceClient.HttpClient
* @private
*/
function createSearchFunction(config, httpRequest) {
return function (searchParameters, onJsonResultReceived) {
var onFailure = function (resultText, httpStatus) {
console.error("search failed with status code " + httpStatus + ": " + resultText);
};
var searchUrl = config.resolveSearchUrl(searchParameters);
var searchBody = config.resolveSearchBody(searchParameters);
var request = { url: searchUrl, method: config.searchMethod, contentType: config.searchContentType, body: searchBody };
if (config.debugMode) {
onJsonResultReceived = loggedSuccess(onJsonResultReceived);
}
httpRequestJson(request, httpRequest, onJsonResultReceived, onFailure);
};
}
function loggedSuccess(onSuccess) {
return function (jsonResult, status) {
console.log("successful search response with code " + status + ": " + JSON.stringify(jsonResult, null, 2));
onSuccess(jsonResult, status);
};
}
/**
* This function will be called when a already parsed response of the HTTP request is available.
* @callback module:searchMenuServiceClient.HttpClient.ParsedHttpResponseAvailable
* @param {Object} resultData already parsed data object containing the results of the HTTP request
* @param {number} httpStatus HTTP response status
*/
/**
* This function will be called when a response of the HTTP request is available as text.
* @callback module:searchMenuServiceClient.HttpClient.TextHttpResponseAvailable
* @param {Object} resultText response body as text
* @param {number} httpStatus HTTP response status
*/
/**
* Executes an HTTP "AJAX" request.
*
* @param {Object} request - flattened json from search query result
* @param {string} request.url - name of the property in hierarchical order separated by points
* @param {string} request.method - value of the property as string
* @param {string} request.contentType - value of the property as string
* @param {string} request.body - value of the property as string
* @param {Object} httpRequest - Browser provided object to use for the HTTP request.
* @param {module:searchMenuServiceClient.HttpClient.ParsedHttpResponseAvailable} onSuccess - will be called when the request was successful.
* @param {module:searchMenuServiceClient.HttpClient.TextHttpResponseAvailable} onFailure - will be called with the error message as text
* @memberof module:searchMenuServiceClient.HttpClient
* @private
*/
function httpRequestJson(request, httpRequest, onSuccess, onFailure) {
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState === 4) {
if (httpRequest.status >= 200 && httpRequest.status <= 299) {
var jsonResult = JSON.parse(httpRequest.responseText);
onSuccess(jsonResult, httpRequest.status);
} else {
onFailure(httpRequest.responseText, httpRequest.status);
}
}
};
httpRequest.open(request.method, request.url, true);
httpRequest.setRequestHeader("Content-Type", request.contentType);
httpRequest.send(request.body);
}
return instance;
}());