http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/package.json ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/package.json b/extensions/web-tracker/javascript/package.json new file mode 100644 index 0000000..0eb46e5 --- /dev/null +++ b/extensions/web-tracker/javascript/package.json @@ -0,0 +1,37 @@ +{ + "name": "unomi-analytics", + "version": "1.0.0", + "description": "The Apache Unomi analytics.js integration.", + "main": "src/index.js", + "keywords": [ + "unomi", + "analytics.js", + "apache" + ], + "author": "Apache Software Foundation", + "license": "Apache-2.0", + "scripts": { + "build": "yarn browserify && yarn minify", + "browserify": "browserify src/index.js -p [ browserify-header --file src/license.js ] -s unomiTracker > dist/unomi-tracker.js", + "minify": "uglifyjs -c -m --comments '/@license/' -o dist/unomi-tracker.min.js -- dist/unomi-tracker.js", + "snippet:minify": "uglifyjs -c -m -o snippet.min.js -- snippet.js", + "clean": "rimraf *.log dist/unomi-tracker.js dist/unomi-tracker.min.js", + "clean:all": "yarn clean && rimraf node_modules" + }, + "dependencies": { + "@segment/analytics.js-core": "^3.7.2", + "@segment/analytics.js-integration": "^2.1.1", + "browserify-header": "^0.9.4" + }, + "devDependencies": { + "@segment/eslint-config": "^3.1.1", + "browserify": "^13.0.1", + "eslint": "^2.9.0", + "eslint-plugin-mocha": "^2.2.0", + "eslint-plugin-require-path-exists": "^1.1.5", + "rimraf": "^2.6.2", + "uglify-js": "^2.6.4", + "watchify": "^3.11.0", + "yarn": "^1.9.4" + } +}
http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/snippet.js ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/snippet.js b/extensions/web-tracker/javascript/snippet.js new file mode 100644 index 0000000..784ccd9 --- /dev/null +++ b/extensions/web-tracker/javascript/snippet.js @@ -0,0 +1,84 @@ +/* + * 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. + */ +window.unomiTracker || (window.unomiTracker = {}); +(function () { + var unomiTracker_queue = []; + + var methods = ['trackSubmit', 'trackClick', 'trackLink', 'trackForm', 'initialize', 'pageview', 'identify', 'reset', 'group', 'track', 'ready', 'alias', 'debug', 'page', 'once', 'off', 'on']; + + var factory = function (method) { + return function () { + var args = Array.prototype.slice.call(arguments); + args.unshift(method); + unomiTracker_queue.push(args); + return window.unomiTracker; + }; + }; + + // For each of our methods, generate a queueing stub. + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + window.unomiTracker[method] = factory(method); + } + + // Define a method to load Analytics.js from our CDN, + // and that will be sure to only ever load it once. + unomiTracker.load = function (callback, option) { + // Create an async script element based on your key. + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + // TODO we might want to add a check on the url to see if it ends with / or not + script.src = option.url + '/tracker/javascript/unomi-tracker.min.js'; + + if (script.addEventListener) { + script.addEventListener('load', function (e) { + if (typeof callback === 'function') { + callback(e); + } + }, false); + } else { + script.onreadystatechange = function () { + if (this.readyState == 'complete' || this.readyState == 'loaded') { + callback(window.event); + } + }; + } + + // Insert our script next to the first script element. + var first = document.getElementsByTagName('script')[0]; + first.parentNode.insertBefore(script, first); + }; + + unomiTracker.load(function (option) { + unomiTracker.initialize({ + 'Apache Unomi': option + }); + + // Loop through the interim analytics queue and reapply the calls to their + // proper analytics.js method. + while (unomiTracker_queue.length > 0) { + var item = unomiTracker_queue.shift(); + var method = item.shift(); + if (unomiTracker[method]) { + unomiTracker[method].apply(unomiTracker, item); + } + } + }, unomiOption); + + unomiTracker.page(); +})(); http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/snippet.min.js ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/snippet.min.js b/extensions/web-tracker/javascript/snippet.min.js new file mode 100644 index 0000000..bd05fb2 --- /dev/null +++ b/extensions/web-tracker/javascript/snippet.min.js @@ -0,0 +1 @@ +window.unomiTracker||(window.unomiTracker={}),function(){for(var e=[],r=["trackSubmit","trackClick","trackLink","trackForm","initialize","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"],n=0;n<r.length;n++){var t=r[n];window.unomiTracker[t]=function(r){return function(){var n=Array.prototype.slice.call(arguments);return n.unshift(r),e.push(n),window.unomiTracker}}(t)}unomiTracker.load=function(e,r){var n=document.createElement("script");n.type="text/javascript",n.async=!0,n.src=r.url+"/tracker/javascript/unomi-tracker.min.js",n.addEventListener?n.addEventListener("load",function(r){"function"==typeof e&&e(r)},!1):n.onreadystatechange=function(){"complete"!=this.readyState&&"loaded"!=this.readyState||e(window.event)};var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(n,t)},unomiTracker.load(function(r){for(unomiTracker.initialize({"Apache Unomi":r});e.length>0;){var n=e.shift(),t=n.shift();unomiTracker[t]&&unomiTra cker[t].apply(unomiTracker,n)}},unomiOption),unomiTracker.page()}(); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/src/analytics.js-integration-apache-unomi.js ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/src/analytics.js-integration-apache-unomi.js b/extensions/web-tracker/javascript/src/analytics.js-integration-apache-unomi.js new file mode 100644 index 0000000..b1b17c5 --- /dev/null +++ b/extensions/web-tracker/javascript/src/analytics.js-integration-apache-unomi.js @@ -0,0 +1,483 @@ +/* + * 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. + */ +'use strict'; + +var integration = require('@segment/analytics.js-integration'); + +var Unomi = module.exports = integration('Apache Unomi') + .assumesPageview() + .readyOnLoad() + .global('cxs') + .option('scope', 'systemscope') + .option('url', 'http://localhost:8181') + .option('timeoutInMilliseconds', 1500) + .option('sessionCookieName', 'unomiSessionId') + .option('sessionId'); + +/** + * Initialize. + * + * @api public + */ +Unomi.prototype.initialize = function(page) { + var self = this; + this.analytics.on('invoke', function(msg) { + var action = msg.action(); + var listener = 'on' + msg.action(); + self.debug('%s %o', action, msg); + if (self[listener]) self[listener](msg); + }); + + // Standard to check if cookies are enabled in this browser + if (!navigator.cookieEnabled) { + this.executeFallback(); + return; + } + + // digitalData come from a standard so we can keep the logic around it which can allow complex website to load more complex data + if (!window.digitalData) { + window.digitalData = { + scope: this.options.scope + }; + } + + if (page) { + var props = page.json().properties; + var unomiPage = window.digitalData.page; + if (!unomiPage) { + unomiPage = window.digitalData.page = { pageInfo:{} } + } + this.fillPageData(unomiPage, props); + if (!window.digitalData.events) { + window.digitalData.events = [] + } + window.digitalData.events.push(this.buildEvent('view', this.buildPage(unomiPage), this.buildSource(this.options.scope, 'site'))) + } + + if (!this.options.sessionId) { + var cookie = require('component-cookie'); + + this.sessionId = cookie(this.options.sessionCookieName); + // so we should not need to implement our own + if (!this.sessionId || this.sessionId === '') { + this.sessionId = this.generateGuid(); + cookie(this.options.sessionCookieName, this.sessionId); + } + } else { + this.sessionId = this.options.sessionId; + } + + + this.loadContext(); +}; + +/** + * Loaded. + * + * @api private + * @return {boolean} + */ +Unomi.prototype.loaded = function() { + return !!window.cxs; +}; + +/** + * Page. + * + * @api public + * @param {Page} page + */ +Unomi.prototype.onpage = function(page) { + console.log('onpage'); + console.log(page); + + var unomiPage = { pageInfo:{} }; + this.fillPageData(unomiPage, page.json().properties); + console.log(unomiPage); + + this.collectEvent(this.buildEvent('view', this.buildPage(unomiPage), this.buildSource(this.options.scope, 'site'))); +}; + +Unomi.prototype.fillPageData = function(unomiPage, props) { + unomiPage.attributes = []; + unomiPage.consentTypes = []; + unomiPage.pageInfo.pageName = unomiPage.pageInfo.pageName || props.title; + unomiPage.pageInfo.pageID = unomiPage.pageInfo.pageID || props.path; + unomiPage.pageInfo.pagePath = unomiPage.pageInfo.pagePath || props.path; + unomiPage.pageInfo.destinationURL = unomiPage.pageInfo.destinationURL || props.url; + unomiPage.pageInfo.referringURL = unomiPage.pageInfo.referringURL || props.referrer; +} + + +/** + * Identify. + * + * @api public + * @param {Identify} identify + */ +Unomi.prototype.onidentify = function(identify) { + console.log('onidentify'); + console.log(identify); + // this.collectEvent(identify.json()); +}; + +/** + * ontrack. + * + * @api private + * @param {Track} track + */ +// TODO: figure out why we need this. +Unomi.prototype.ontrack = function(track) { + console.log('ontrack'); + console.log(track); + // var json = track.json(); + + // delete json.traits; + // this.collectEvent(json); +}; + +/** + * This function is used to load the current context in the page + * + * @param {boolean} [skipEvents=false] Should we send the events + * @param {boolean} [invalidate=false] Should we invalidate the current context + */ +Unomi.prototype.loadContext = function (skipEvents, invalidate) { + var jsonData = { + requiredProfileProperties: ['j:nodename'], + source: this.buildPage(window.digitalData.page) + }; + if (!skipEvents) { + jsonData.events = window.digitalData.events + } + if (window.digitalData.personalizationCallback) { + jsonData.personalizations = window.digitalData.personalizationCallback.map(function (x) { + return x.personalization + }) + } + + jsonData.sessionId = this.sessionId; + + var contextUrl = this.options.url + '/context.json'; + if (invalidate) { + contextUrl += '?invalidateSession=true&invalidateProfile=true'; + } + + var self = this; + + var onSuccess = function (xhr) { + + window.cxs = JSON.parse(xhr.responseText); + + self.ready(); + + if (window.digitalData.loadCallbacks && window.digitalData.loadCallbacks.length > 0) { + console.info('[UNOMI] Found context server load callbacks, calling now...'); + if (window.digitalData.loadCallbacks) { + for (var i = 0; i < window.digitalData.loadCallbacks.length; i++) { + window.digitalData.loadCallbacks[i](digitalData); + } + } + if (window.digitalData.personalizationCallback) { + for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) { + window.digitalData.personalizationCallback[i].callback(cxs.personalizations[window.digitalData.personalizationCallback[i].personalization.id]); + } + } + } + }; + + this.ajax({ + url: contextUrl, + type: 'POST', + async: true, + contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight + jsonData: jsonData, + dataType: 'application/json', + invalidate: invalidate, + success: onSuccess, + error: this.executeFallback + }); + + console.info('[UNOMI] context loading...'); +}; + +/** + * This function return the basic structure for an event, it must be adapted to your need + * + * @param {string} eventType The name of your event + * @param {object} [target] The target object for your event can be build with this.buildTarget(targetId, targetType, targetProperties) + * @param {object} [source] The source object for your event can be build with this.buildSource(sourceId, sourceType, sourceProperties) + * @returns {{eventType: *, scope}} + */ +Unomi.prototype.buildEvent = function (eventType, target, source) { + var event = { + eventType: eventType, + scope: window.digitalData.scope + }; + + if (target) { + event.target = target; + } + + if (source) { + event.source = source; + } + + return event; +}; + +/** + * This function return an event of type form + * + * @param {string} formName The HTML name of id of the form to use in the target of the event + * @returns {*|{eventType: *, scope, source: {scope, itemId: string, itemType: string, properties: {}}, target: {scope, itemId: string, itemType: string, properties: {}}}} + */ +Unomi.prototype.buildFormEvent = function (formName) { + return this.buildEvent('form', this.buildTarget(formName, 'form'), this.buildSourcePage()); +}; + +/** + * This function return the source object for a source of type page + * + * @returns {*|{scope, itemId: *, itemType: *}} + */ +Unomi.prototype.buildTargetPage = function () { + return this.buildTarget(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page); +}; + +/** + * This function return the source object for a source of type page + * + * @returns {*|{scope, itemId: *, itemType: *}} + */ +Unomi.prototype.buildPage = function (page) { + return this.buildSource(page.pageInfo.pageID, 'page', page); +}; + +/** + * This function return the basic structure for the target of your event + * + * @param {string} targetId The ID of the target + * @param {string} targetType The type of the target + * @param {object} [targetProperties] The optional properties of the target + * @returns {{scope, itemId: *, itemType: *}} + */ +Unomi.prototype.buildTarget = function (targetId, targetType, targetProperties) { + return this.buildObject(targetId, targetType, targetProperties); +}; + +/** + * This function return the basic structure for the source of your event + * + * @param {string} sourceId The ID of the source + * @param {string} sourceType The type of the source + * @param {object} [sourceProperties] The optional properties of the source + * @returns {{scope, itemId: *, itemType: *}} + */ +Unomi.prototype.buildSource = function (sourceId, sourceType, sourceProperties) { + return this.buildObject(sourceId, sourceType, sourceProperties); +}; + + +/** + * This function will send an event to Apache Unomi + * @param {object} event The event object to send, you can build it using this.buildEvent(eventType, target, source) + * @param {function} successCallback will be executed in case of success + * @param {function} errorCallback will be executed in case of error + */ +Unomi.prototype.collectEvent = function (event, successCallback, errorCallback) { + this.collectEvents({events: [event]}, successCallback, errorCallback); +}; + +/** + * This function will send the events to Apache Unomi + * + * @param {object} events Javascript object { events: [event1, event2] } + * @param {function} successCallback will be executed in case of success + * @param {function} errorCallback will be executed in case of error + */ +Unomi.prototype.collectEvents = function (events, successCallback, errorCallback) { + events.sessionId = this.sessionId; + + var data = JSON.stringify(events); + this.ajax({ + url: this.options.url + '/eventcollector', + type: 'POST', + async: true, + contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight + data: data, + dataType: 'application/json', + success: successCallback, + error: errorCallback + }); +}; + +/*******************************/ +/* Private Function under this */ +/*******************************/ + +Unomi.prototype.registerEvent = function (event) { + if (window.digitalData) { + if (window.cxs) { + console.error('[UNOMI] already loaded, too late...'); + } else { + window.digitalData.events = window.digitalData.events || []; + window.digitalData.events.push(event); + } + } else { + window.digitalData = {}; + window.digitalData.events = window.digitalData.events || []; + window.digitalData.events.push(event); + } +}; + +Unomi.prototype.registerCallback = function (onLoadCallback) { + if (window.digitalData) { + if (window.cxs) { + console.info('[UNOMI] digitalData object loaded, calling on load callback immediately and registering update callback...'); + if (onLoadCallback) { + onLoadCallback(window.digitalData); + } + } else { + console.info('[UNOMI] digitalData object present but not loaded, registering load callback...'); + if (onLoadCallback) { + window.digitalData.loadCallbacks = window.digitalData.loadCallbacks || []; + window.digitalData.loadCallbacks.push(onLoadCallback); + } + } + } else { + console.info('[UNOMI] No digital data object found, creating and registering update callback...'); + window.digitalData = {}; + if (onLoadCallback) { + window.digitalData.loadCallbacks = []; + window.digitalData.loadCallbacks.push(onLoadCallback); + } + } +}; + +/** + * This is an utility function to generate a new UUID + * + * @returns {string} + */ +Unomi.prototype.generateGuid = function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); +}; + +Unomi.prototype.buildObject = function (itemId, itemType, properties) { + var object = { + scope: window.digitalData.scope, + itemId: itemId, + itemType: itemType + }; + + if (properties) { + object.properties = properties; + } + + return object; +}; + +/** + * This is an utility function to execute AJAX call + * + * @param {object} ajaxOptions + */ +Unomi.prototype.ajax = function (ajaxOptions) { + var xhr = new XMLHttpRequest(); + if ('withCredentials' in xhr) { + xhr.open(ajaxOptions.type, ajaxOptions.url, ajaxOptions.async); + xhr.withCredentials = true; + } else if (typeof XDomainRequest != 'undefined') { + xhr = new XDomainRequest(); + xhr.open(ajaxOptions.type, ajaxOptions.url); + } + + if (ajaxOptions.contentType) { + xhr.setRequestHeader('Content-Type', ajaxOptions.contentType); + } + if (ajaxOptions.dataType) { + xhr.setRequestHeader('Accept', ajaxOptions.dataType); + } + + if (ajaxOptions.responseType) { + xhr.responseType = ajaxOptions.responseType; + } + + var requestExecuted = false; + if (this.options.timeoutInMilliseconds !== -1) { + setTimeout(function () { + if (!requestExecuted) { + console.error('[UNOMI] XML request timeout, url: ' + ajaxOptions.url); + requestExecuted = true; + if (ajaxOptions.error) { + ajaxOptions.error(xhr); + } + } + }, this.options.timeoutInMilliseconds); + } + + xhr.onreadystatechange = function () { + if (!requestExecuted) { + if (xhr.readyState === 4) { + if (xhr.status === 200 || xhr.status === 204 || xhr.status === 304) { + if (xhr.responseText != null) { + requestExecuted = true; + if (ajaxOptions.success) { + ajaxOptions.success(xhr); + } + } + } else { + requestExecuted = true; + if (ajaxOptions.error) { + ajaxOptions.error(xhr); + } + console.error('[UNOMI] XML request error: ' + xhr.statusText + ' (' + xhr.status + ')'); + } + } + } + }; + + if (ajaxOptions.jsonData) { + xhr.send(JSON.stringify(ajaxOptions.jsonData)); + } else if (ajaxOptions.data) { + xhr.send(ajaxOptions.data); + } else { + xhr.send(); + } +}; + +Unomi.prototype.executeFallback = function () { + console.warn('[UNOMI] execute fallback'); + window.cxs = {}; + for (var index in window.digitalData.loadCallbacks) { + window.digitalData.loadCallbacks[index](); + } + if (window.digitalData.personalizationCallback) { + for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) { + window.digitalData.personalizationCallback[i].callback([window.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]); + } + } +}; http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/src/index.js ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/src/index.js b/extensions/web-tracker/javascript/src/index.js new file mode 100644 index 0000000..c857557 --- /dev/null +++ b/extensions/web-tracker/javascript/src/index.js @@ -0,0 +1,50 @@ +/* + * 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. + */ +/** + * Analytics.js + * + * (C) 2017 Segment Inc. + */ + +var analytics = require('@segment/analytics.js-core'); +var Integrations = require('./integrations'); + +/** + * Expose the `analytics` singleton. + */ + +module.exports = exports = analytics; + +/** + * Expose require. + */ + +analytics.require = require; + +/** + * Expose `VERSION`. + */ + +exports.VERSION = require('../package.json').version; + +/** + * Add integrations. + */ + +for (var integration in Integrations) { + analytics.use(Integrations[integration]); +} http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/src/integrations.js ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/src/integrations.js b/extensions/web-tracker/javascript/src/integrations.js new file mode 100644 index 0000000..7d2ebcd --- /dev/null +++ b/extensions/web-tracker/javascript/src/integrations.js @@ -0,0 +1,22 @@ +/* + * 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. + */ +/* eslint quote-props: 0 */ +'use strict'; + +module.exports = { + 'apache-unomi': require('./analytics.js-integration-apache-unomi') +}; http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/83aa4115/extensions/web-tracker/javascript/src/license.js ---------------------------------------------------------------------- diff --git a/extensions/web-tracker/javascript/src/license.js b/extensions/web-tracker/javascript/src/license.js new file mode 100644 index 0000000..9223e0a --- /dev/null +++ b/extensions/web-tracker/javascript/src/license.js @@ -0,0 +1,18 @@ +/*! + * 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. + * + * @license Apache-2.0 + */