Source: server/server-client.js

const HttpClient = require('../http/http-client');
const HttpResponse = require('../http/http-response');
const ConsoleLogger = require('../logging/console-logger');
const LoggingUtils = require('../logging/logging-utils');
const RDFRepositoryClient = require('../repository/rdf-repository-client');
const RepositoryClientConfig =
  require('../repository/repository-client-config');
const HttpRequestBuilder = require('../http/http-request-builder');
const AuthenticationService = require('../service/authentication-service');
const RDFMimeType = require('../http/rdf-mime-type');

const SERVICE_URL = '/repositories';

/**
 * Implementation of the server operations.
 *
 * If the server against which this client will be used has security enabled,
 * then it should be configured with the username and password in the
 * {@link ServerClientConfig}. In this case a login attempt is made before any
 * API method to be executed. Upon successful login an {@link User} which holds
 * the credentials and the authorization token in the context of the client is
 * created. In all consecutive API calls the authorization token is sent as a
 * http header.
 *
 * By default {@link ServerClientConfig} is configured with
 * <code>keepAlive = true</code> which means that upon authorization token
 * expiration current logged in user would be re-logged automatically before
 * next API call. This configuration can be changed within the configuration.
 *
 * @class
 * @author Mihail Radkov
 * @author Svilen Velikov
 */
class ServerClient {
  /**
   * @param {ServerClientConfig} config for the server client.
   **/
  constructor(config) {
    this.config = config;

    this.initHttpClient();
    this.initLogger();

    this.authenticationService = new AuthenticationService(this.httpClient);
  }

  /**
   * Get an array of repository ids available in the server.
   *
   * @return {Promise<Array>} promise which resolves with an Array with
   * repository ids.
   */
  getRepositoryIDs() {
    const requestBuilder = HttpRequestBuilder.httpGet(SERVICE_URL)
      .addAcceptHeader(RDFMimeType.SPARQL_RESULTS_JSON);
    return this.execute(requestBuilder).then((response) => {
      this.logger.debug(LoggingUtils.getLogPayload(response, {}),
        'Fetched repositories');
      return response.getData().results.bindings.map(({id}) => id.value);
    });
  }

  /**
   * Check if repository with the provided id exists.
   * @param {string} id of the repository which should be checked.
   * @return {Promise<boolean>} promise which resolves with boolean value.
   */
  hasRepository(id) {
    if (!id) {
      throw new Error('Repository id is required parameter!');
    }
    return this.getRepositoryIDs().then((repositoryIds) => {
      return repositoryIds.indexOf(id) !== -1;
    });
  }

  /**
   * Creates a repository client instance with the provided id and
   * configuration.
   * @param {string} id of the repository
   * @param {RepositoryClientConfig} config for the overridable repository
   *    configuration.
   * @return {Promise<RDFRepositoryClient>} promise which resolves with
   *    new RDFRepositoryClient instance.
   */
  getRepository(id, config) {
    if (!id) {
      throw new Error('Repository id is required parameter!');
    }
    if (!config || !(config instanceof RepositoryClientConfig)) {
      throw new Error('RepositoryClientConfig is required parameter!');
    }
    return this.hasRepository(id).then((exists) => {
      if (exists) {
        return new RDFRepositoryClient(config);
      }
      this.logger.error({repoId: id}, 'Repository does not exist');
      return Promise
        .reject(new Error(`Repository with id ${id} does not exists.`));
    });
  }

  /**
   * Delete repository with the provided id.
   * @param {string} id of the repository which should be deleted.
   * @return {Promise<any>} promise which resolves with the delete result.
   */
  deleteRepository(id) {
    if (!id) {
      throw new Error('Repository id is required parameter!');
    }
    const requestBuilder =
      HttpRequestBuilder.httpDelete(`${SERVICE_URL}/${id}`);
    return this.execute(requestBuilder).then((response) => {
      this.logger.info(LoggingUtils.getLogPayload(response, {repoId: id}),
        'Deleted repository');
    });
  }

  /**
   * Initializes the http client.
   */
  initHttpClient() {
    this.httpClient = new HttpClient(this.config.getEndpoint())
      .setDefaultReadTimeout(this.config.getTimeout())
      .setDefaultWriteTimeout(this.config.getTimeout());
  }

  /**
   * Initializes the logger.
   */
  initLogger() {
    this.logger = new ConsoleLogger({
      name: 'ServerClient',
      serverURL: this.config.getEndpoint()
    });
  }

  /**
   * Executes http request wrapped in provided request builder.
   * If the server config provides username and password, then a logging attempt
   * is made. Upon successful login the auth data is stored for later requests.
   *
   * @public
   *
   * @param {HttpRequestBuilder} requestBuilder
   *
   * @return {Promise<HttpResponse|Error>} a promise which resolves to response
   * wrapper or rejects with error if thrown during execution.
   */
  execute(requestBuilder) {
    const startTime = Date.now();
    return this.authenticationService.login(this.config, this.getLoggedUser())
      .then((user) => {
        this.setLoggedUser(user);
        this.decorateRequestConfig(requestBuilder);
        return this.httpClient.request(requestBuilder);
      })
      .then((response) => {
        const executionResponse = new HttpResponse(response, this.httpClient);
        executionResponse.setElapsedTime(Date.now() - startTime);
        return executionResponse;
      })
      .catch((error) => {
        const status = error.response ? error.response.status : null;
        // Unauthorized
        if (status && status === 401 && this.config.getKeepAlive()) {
          // re-execute will try to re-login the user and update it
          return this.execute(requestBuilder);
        }
        return Promise.reject(error);
      });
  }

  /**
   * Performs a logout of logged in user.
   *
   * This method normally shouldn't be called as it does nothing but just clears
   * current authentication token. After that any consecutive API call against
   * the secured server will throw <code>Unauthorized</code> error with status
   * code <code>401</code> because the token is not sent any more, which in
   * result will force re-login for the same user to be executed by default,
   * unless the client is configured with
   * <code>ServerClientConfig.keepAlive = false</code>
   *
   * @private
   *
   * @return {Promise} returns a promise which resolves with undefined.
   */
  logout() {
    return this.authenticationService.logout(this.getLoggedUser());
  }

  /**
   * Allow request config to be altered before sending.
   *
   * @private
   * @param {HttpRequestBuilder} requestBuilder
   */
  decorateRequestConfig(requestBuilder) {
    const token = this.authenticationService
      .getAuthenticationToken(this.getLoggedUser());
    if (token) {
      requestBuilder.addAuthorizationHeader(token);
    }
  }

  /**
   * Logged user getter.
   * @return {User} user
   */
  getLoggedUser() {
    return this.user;
  }

  /**
   * User setter
   * @param {User} user
   *
   * @return {ServerClient}
   */
  setLoggedUser(user) {
    this.user = user;
    return this;
  }
}

module.exports = ServerClient;