Enabled and fixed for `check-alignment`, `implements-on-classes`, `newline-after-description`, `require-param-name`, `require-param-type`, `require-returns-type` and `valid-types`. Change-Id: Icb0533e44e17b10246f1b9d33f8d01f16a1e15a5
		
			
				
	
	
		
			432 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			432 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @license
 | 
						|
 * Copyright (C) 2019 The Android Open Source Project
 | 
						|
 *
 | 
						|
 * Licensed 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.
 | 
						|
 */
 | 
						|
(function(window) {
 | 
						|
  'use strict';
 | 
						|
 | 
						|
  const JSON_PREFIX = ')]}\'';
 | 
						|
  const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
 | 
						|
 | 
						|
  /**
 | 
						|
   * Wrapper around Map for caching server responses. Site-based so that
 | 
						|
   * changes to CANONICAL_PATH will result in a different cache going into
 | 
						|
   * effect.
 | 
						|
   */
 | 
						|
  class SiteBasedCache {
 | 
						|
    constructor() {
 | 
						|
      // Container of per-canonical-path caches.
 | 
						|
      this._data = new Map();
 | 
						|
      if (window.INITIAL_DATA != undefined) {
 | 
						|
        // Put all data shipped with index.html into the cache. This makes it
 | 
						|
        // so that we spare more round trips to the server when the app loads
 | 
						|
        // initially.
 | 
						|
        Object
 | 
						|
            .entries(window.INITIAL_DATA)
 | 
						|
            .forEach(e => this._cache().set(e[0], e[1]));
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Returns the cache for the current canonical path.
 | 
						|
    _cache() {
 | 
						|
      if (!this._data.has(window.CANONICAL_PATH)) {
 | 
						|
        this._data.set(window.CANONICAL_PATH, new Map());
 | 
						|
      }
 | 
						|
      return this._data.get(window.CANONICAL_PATH);
 | 
						|
    }
 | 
						|
 | 
						|
    has(key) {
 | 
						|
      return this._cache().has(key);
 | 
						|
    }
 | 
						|
 | 
						|
    get(key) {
 | 
						|
      return this._cache().get(key);
 | 
						|
    }
 | 
						|
 | 
						|
    set(key, value) {
 | 
						|
      this._cache().set(key, value);
 | 
						|
    }
 | 
						|
 | 
						|
    delete(key) {
 | 
						|
      this._cache().delete(key);
 | 
						|
    }
 | 
						|
 | 
						|
    invalidatePrefix(prefix) {
 | 
						|
      const newMap = new Map();
 | 
						|
      for (const [key, value] of this._cache().entries()) {
 | 
						|
        if (!key.startsWith(prefix)) {
 | 
						|
          newMap.set(key, value);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      this._data.set(window.CANONICAL_PATH, newMap);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  class FetchPromisesCache {
 | 
						|
    constructor() {
 | 
						|
      this._data = {};
 | 
						|
    }
 | 
						|
 | 
						|
    has(key) {
 | 
						|
      return !!this._data[key];
 | 
						|
    }
 | 
						|
 | 
						|
    get(key) {
 | 
						|
      return this._data[key];
 | 
						|
    }
 | 
						|
 | 
						|
    set(key, value) {
 | 
						|
      this._data[key] = value;
 | 
						|
    }
 | 
						|
 | 
						|
    invalidatePrefix(prefix) {
 | 
						|
      const newData = {};
 | 
						|
      Object.entries(this._data).forEach(([key, value]) => {
 | 
						|
        if (!key.startsWith(prefix)) {
 | 
						|
          newData[key] = value;
 | 
						|
        }
 | 
						|
      });
 | 
						|
      this._data = newData;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  class GrRestApiHelper {
 | 
						|
    /**
 | 
						|
     * @param {SiteBasedCache} cache
 | 
						|
     * @param {object} auth
 | 
						|
     * @param {FetchPromisesCache} fetchPromisesCache
 | 
						|
     * @param {object} credentialCheck
 | 
						|
     * @param {object} restApiInterface
 | 
						|
     */
 | 
						|
    constructor(cache, auth, fetchPromisesCache, credentialCheck,
 | 
						|
        restApiInterface) {
 | 
						|
      this._cache = cache;// TODO: make it public
 | 
						|
      this._auth = auth;
 | 
						|
      this._fetchPromisesCache = fetchPromisesCache;
 | 
						|
      this._credentialCheck = credentialCheck;
 | 
						|
      this._restApiInterface = restApiInterface;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
 | 
						|
     * with timing and logging.
 | 
						|
     *
 | 
						|
     * @param {Gerrit.FetchRequest} req
 | 
						|
     */
 | 
						|
    fetch(req) {
 | 
						|
      const start = Date.now();
 | 
						|
      const xhr = this._auth.fetch(req.url, req.fetchOptions);
 | 
						|
 | 
						|
      // Log the call after it completes.
 | 
						|
      xhr.then(res => this._logCall(req, start, res ? res.status : null));
 | 
						|
 | 
						|
      // Return the XHR directly (without the log).
 | 
						|
      return xhr;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Log information about a REST call. Because the elapsed time is determined
 | 
						|
     * by this method, it should be called immediately after the request
 | 
						|
     * finishes.
 | 
						|
     *
 | 
						|
     * @param {Gerrit.FetchRequest} req
 | 
						|
     * @param {number} startTime the time that the request was started.
 | 
						|
     * @param {number} status the HTTP status of the response. The status value
 | 
						|
     *     is used here rather than the response object so there is no way this
 | 
						|
     *     method can read the body stream.
 | 
						|
     */
 | 
						|
    _logCall(req, startTime, status) {
 | 
						|
      const method = (req.fetchOptions && req.fetchOptions.method) ?
 | 
						|
        req.fetchOptions.method : 'GET';
 | 
						|
      const endTime = Date.now();
 | 
						|
      const elapsed = (endTime - startTime);
 | 
						|
      const startAt = new Date(startTime);
 | 
						|
      const endAt = new Date(endTime);
 | 
						|
      console.log([
 | 
						|
        'HTTP',
 | 
						|
        status,
 | 
						|
        method,
 | 
						|
        elapsed + 'ms',
 | 
						|
        req.anonymizedUrl || req.url,
 | 
						|
        `(${startAt.toISOString()}, ${endAt.toISOString()})`,
 | 
						|
      ].join(' '));
 | 
						|
      if (req.anonymizedUrl) {
 | 
						|
        this.fire('rpc-log',
 | 
						|
            {status, method, elapsed, anonymizedUrl: req.anonymizedUrl});
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Fetch JSON from url provided.
 | 
						|
     * Returns a Promise that resolves to a native Response.
 | 
						|
     * Doesn't do error checking. Supports cancel condition. Performs auth.
 | 
						|
     * Validates auth expiry errors.
 | 
						|
     *
 | 
						|
     * @param {Gerrit.FetchJSONRequest} req
 | 
						|
     */
 | 
						|
    fetchRawJSON(req) {
 | 
						|
      const urlWithParams = this.urlWithParams(req.url, req.params);
 | 
						|
      const fetchReq = {
 | 
						|
        url: urlWithParams,
 | 
						|
        fetchOptions: req.fetchOptions,
 | 
						|
        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
 | 
						|
      };
 | 
						|
      return this.fetch(fetchReq).then(res => {
 | 
						|
        if (req.cancelCondition && req.cancelCondition()) {
 | 
						|
          res.body.cancel();
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        return res;
 | 
						|
      }).catch(err => {
 | 
						|
        const isLoggedIn = !!this._cache.get('/accounts/self/detail');
 | 
						|
        if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
 | 
						|
          this.checkCredentials();
 | 
						|
        } else {
 | 
						|
          if (req.errFn) {
 | 
						|
            req.errFn.call(undefined, null, err);
 | 
						|
          } else {
 | 
						|
            this.fire('network-error', {error: err});
 | 
						|
          }
 | 
						|
        }
 | 
						|
        throw err;
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Fetch JSON from url provided.
 | 
						|
     * Returns a Promise that resolves to a parsed response.
 | 
						|
     * Same as {@link fetchRawJSON}, plus error handling.
 | 
						|
     *
 | 
						|
     * @param {Gerrit.FetchJSONRequest} req
 | 
						|
     */
 | 
						|
    fetchJSON(req) {
 | 
						|
      req = this.addAcceptJsonHeader(req);
 | 
						|
      return this.fetchRawJSON(req).then(response => {
 | 
						|
        if (!response) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        if (!response.ok) {
 | 
						|
          if (req.errFn) {
 | 
						|
            req.errFn.call(null, response);
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          this.fire('server-error', {request: req, response});
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        return response && this.getResponseObject(response);
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {string} url
 | 
						|
     * @param {?Object|string=} opt_params URL params, key-value hash.
 | 
						|
     * @return {string}
 | 
						|
     */
 | 
						|
    urlWithParams(url, opt_params) {
 | 
						|
      if (!opt_params) { return this.getBaseUrl() + url; }
 | 
						|
 | 
						|
      const params = [];
 | 
						|
      for (const p in opt_params) {
 | 
						|
        if (!opt_params.hasOwnProperty(p)) { continue; }
 | 
						|
        if (opt_params[p] == null) {
 | 
						|
          params.push(encodeURIComponent(p));
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
        for (const value of [].concat(opt_params[p])) {
 | 
						|
          params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      return this.getBaseUrl() + url + '?' + params.join('&');
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {!Object} response
 | 
						|
     * @return {?}
 | 
						|
     */
 | 
						|
    getResponseObject(response) {
 | 
						|
      return this.readResponsePayload(response)
 | 
						|
          .then(payload => payload.parsed);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {!Object} response
 | 
						|
     * @return {!Object}
 | 
						|
     */
 | 
						|
    readResponsePayload(response) {
 | 
						|
      return response.text().then(text => {
 | 
						|
        let result;
 | 
						|
        try {
 | 
						|
          result = this.parsePrefixedJSON(text);
 | 
						|
        } catch (_) {
 | 
						|
          result = null;
 | 
						|
        }
 | 
						|
        return {parsed: result, raw: text};
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {string} source
 | 
						|
     * @return {?}
 | 
						|
     */
 | 
						|
    parsePrefixedJSON(source) {
 | 
						|
      return JSON.parse(source.substring(JSON_PREFIX.length));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {Gerrit.FetchJSONRequest} req
 | 
						|
     * @return {Gerrit.FetchJSONRequest}
 | 
						|
     */
 | 
						|
    addAcceptJsonHeader(req) {
 | 
						|
      if (!req.fetchOptions) req.fetchOptions = {};
 | 
						|
      if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
 | 
						|
      if (!req.fetchOptions.headers.has('Accept')) {
 | 
						|
        req.fetchOptions.headers.append('Accept', 'application/json');
 | 
						|
      }
 | 
						|
      return req;
 | 
						|
    }
 | 
						|
 | 
						|
    getBaseUrl() {
 | 
						|
      return this._restApiInterface.getBaseUrl();
 | 
						|
    }
 | 
						|
 | 
						|
    fire(type, detail, options) {
 | 
						|
      return this._restApiInterface.fire(type, detail, options);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {Gerrit.FetchJSONRequest} req
 | 
						|
     */
 | 
						|
    fetchCacheURL(req) {
 | 
						|
      if (this._fetchPromisesCache.has(req.url)) {
 | 
						|
        return this._fetchPromisesCache.get(req.url);
 | 
						|
      }
 | 
						|
      // TODO(andybons): Periodic cache invalidation.
 | 
						|
      if (this._cache.has(req.url)) {
 | 
						|
        return Promise.resolve(this._cache.get(req.url));
 | 
						|
      }
 | 
						|
      this._fetchPromisesCache.set(req.url,
 | 
						|
          this.fetchJSON(req).then(response => {
 | 
						|
            if (response !== undefined) {
 | 
						|
              this._cache.set(req.url, response);
 | 
						|
            }
 | 
						|
            this._fetchPromisesCache.set(req.url, undefined);
 | 
						|
            return response;
 | 
						|
          }).catch(err => {
 | 
						|
            this._fetchPromisesCache.set(req.url, undefined);
 | 
						|
            throw err;
 | 
						|
          })
 | 
						|
      );
 | 
						|
      return this._fetchPromisesCache.get(req.url);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Send an XHR.
 | 
						|
     *
 | 
						|
     * @param {Gerrit.SendRequest} req
 | 
						|
     * @return {Promise}
 | 
						|
     */
 | 
						|
    send(req) {
 | 
						|
      const options = {method: req.method};
 | 
						|
      if (req.body) {
 | 
						|
        options.headers = new Headers();
 | 
						|
        options.headers.set(
 | 
						|
            'Content-Type', req.contentType || 'application/json');
 | 
						|
        options.body = typeof req.body === 'string' ?
 | 
						|
          req.body : JSON.stringify(req.body);
 | 
						|
      }
 | 
						|
      if (req.headers) {
 | 
						|
        if (!options.headers) { options.headers = new Headers(); }
 | 
						|
        for (const header in req.headers) {
 | 
						|
          if (!req.headers.hasOwnProperty(header)) { continue; }
 | 
						|
          options.headers.set(header, req.headers[header]);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      const url = req.url.startsWith('http') ?
 | 
						|
        req.url : this.getBaseUrl() + req.url;
 | 
						|
      const fetchReq = {
 | 
						|
        url,
 | 
						|
        fetchOptions: options,
 | 
						|
        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
 | 
						|
      };
 | 
						|
      const xhr = this.fetch(fetchReq).then(response => {
 | 
						|
        if (!response.ok) {
 | 
						|
          if (req.errFn) {
 | 
						|
            return req.errFn.call(undefined, response);
 | 
						|
          }
 | 
						|
          this.fire('server-error', {request: fetchReq, response});
 | 
						|
        }
 | 
						|
        return response;
 | 
						|
      }).catch(err => {
 | 
						|
        this.fire('network-error', {error: err});
 | 
						|
        if (req.errFn) {
 | 
						|
          return req.errFn.call(undefined, null, err);
 | 
						|
        } else {
 | 
						|
          throw err;
 | 
						|
        }
 | 
						|
      });
 | 
						|
 | 
						|
      if (req.parseResponse) {
 | 
						|
        return xhr.then(res => this.getResponseObject(res));
 | 
						|
      }
 | 
						|
 | 
						|
      return xhr;
 | 
						|
    }
 | 
						|
 | 
						|
    checkCredentials() {
 | 
						|
      if (this._credentialCheck.checking) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      this._credentialCheck.checking = true;
 | 
						|
      let req = {url: '/accounts/self/detail', reportUrlAsIs: true};
 | 
						|
      req = this.addAcceptJsonHeader(req);
 | 
						|
      // Skip the REST response cache.
 | 
						|
      return this.fetchRawJSON(req).then(res => {
 | 
						|
        if (!res) { return; }
 | 
						|
        if (res.status === 403) {
 | 
						|
          this.fire('auth-error');
 | 
						|
          this._cache.delete('/accounts/self/detail');
 | 
						|
        } else if (res.ok) {
 | 
						|
          return this.getResponseObject(res);
 | 
						|
        }
 | 
						|
      }).then(res => {
 | 
						|
        this._credentialCheck.checking = false;
 | 
						|
        if (res) {
 | 
						|
          this._cache.set('/accounts/self/detail', res);
 | 
						|
        }
 | 
						|
        return res;
 | 
						|
      }).catch(err => {
 | 
						|
        this._credentialCheck.checking = false;
 | 
						|
        if (err && err.message === FAILED_TO_FETCH_ERROR) {
 | 
						|
          this.fire('auth-error');
 | 
						|
          this._cache.delete('/accounts/self/detail');
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {string} prefix
 | 
						|
     */
 | 
						|
    invalidateFetchPromisesPrefix(prefix) {
 | 
						|
      this._fetchPromisesCache.invalidatePrefix(prefix);
 | 
						|
      this._cache.invalidatePrefix(prefix);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  window.SiteBasedCache = SiteBasedCache;
 | 
						|
  window.FetchPromisesCache = FetchPromisesCache;
 | 
						|
  window.GrRestApiHelper = GrRestApiHelper;
 | 
						|
})(window);
 | 
						|
 |