Source: src/style.js

import fetch from 'node-fetch';
import WorkspaceClient from './workspace.js';
import { getGeoServerResponseText, GeoServerResponseError } from './util/geoserver.js';
import AboutClient from './about.js'

/**
 * Client for GeoServer styles
 *
 * @module StyleClient
 */
export default class StyleClient {
  /**
   * Creates a GeoServer REST StyleClient instance.
   *
   * @param {String} url The URL of the GeoServer REST API endpoint
   * @param {String} auth The Basic Authentication string
   */
  constructor (url, auth) {
    this.url = url;
    this.auth = auth;
  }

  /**
   * Returns all default styles.
   *
   * @throws Error if request fails
   *
   * @returns {Object} An object with the default styles
   */
  async getDefaults () {
    const response = await fetch(this.url + 'styles.json', {
      credentials: 'include',
      method: 'GET',
      headers: {
        Authorization: this.auth
      }
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
    return response.json();
  }

  /**
   * Returns all styles in a workspace.
   *
   * @param {String} workspace Workspace name to get styles for
   *
   * @throws Error if request fails
   *
   * @returns {Object} An object with all styles
   */
  async getInWorkspace (workspace) {
    const response = await fetch(this.url + 'workspaces/' + workspace + '/styles.json', {
      credentials: 'include',
      method: 'GET',
      headers: {
        Authorization: this.auth
      }
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
    return response.json();
  }

  /**
   * Returns all styles defined in workspaces.
   *
   * @throws Error if request fails
   *
   * @returns {Object[]} An array with all style objects
   */
  async getAllWorkspaceStyles () {
    const allStyles = [];
    const ws = new WorkspaceClient(this.url, this.auth);
    const allWs = await ws.getAll();

    if (!allWs ||
      !allWs.workspaces ||
      !allWs.workspaces.workspace ||
      !Array.isArray(allWs.workspaces.workspace)) {
      throw new GeoServerResponseError('Response of available workspaces is malformed');
    }

    // go over all workspaces and query the styles for
    for (let i = 0; i < allWs.workspaces.workspace.length; i++) {
      const ws = allWs.workspaces.workspace[i];
      const wsStyles = await this.getInWorkspace(ws.name);

      if (wsStyles.styles.style) {
        wsStyles.styles.style.forEach(wsStyle => {
          allStyles.push(wsStyle);
        });
      }
    }

    return allStyles;
  }

  /**
   * Returns all styles as combined object (default ones and those in
   * workspaces).
   *
   * @returns {Object[]} An array with all style objects
   */
  async getAll () {
    const defaultStyles = await this.getDefaults();
    const wsStyles = await this.getAllWorkspaceStyles();
    if (
      !defaultStyles ||
      !defaultStyles.styles ||
      !defaultStyles.styles.style ||
      !Array.isArray(defaultStyles.styles.style)
    ) {
      throw new GeoServerResponseError('Response of default styles malformed')
    }
    const allStyles = defaultStyles.styles.style.concat(wsStyles);

    return allStyles;
  }

  /**
   * Publishes a new SLD style.
   *
   * @param {String} workspace The workspace to publish the style in
   * @param {String} name Name of the style
   * @param {String} sldBody SLD style (as XML text)
   *
   * @throws Error if request fails
   */
  async publish (workspace, name, sldBody) {
    const response = await fetch(this.url + 'workspaces/' + workspace + '/styles?name=' + name, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/vnd.ogc.sld+xml'
      },
      body: sldBody
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Deletes a style.
   *
   * @param {String} workspace The name of the workspace, can be undefined if style is not assigned to a workspace
   * @param {String} name The name of the style to delete
   * @param {Boolean} [recurse=false] If references to the specified style in existing layers should be deleted
   * @param {Boolean} [purge=false] Whether the underlying file containing the style should be deleted on disk
   */
  async delete (workspace, name, recurse, purge) {
    let paramPurge = false;
    let paramRecurse = false;

    if (purge === true) {
      paramPurge = true;
    }
    if (recurse === true) {
      paramRecurse = true;
    }

    let endpoint;

    if (workspace) {
      // delete style inside workspace
      endpoint = this.url + 'workspaces/' + workspace + '/styles/' + name +
                  '?' + 'purge=' + paramPurge + '&' + 'recurse=' + paramRecurse;
    } else {
      // delete style without workspace
      endpoint = this.url + 'styles/' + name +
                  '?' + 'purge=' + paramPurge + '&' + 'recurse=' + paramRecurse;
    }

    const response = await fetch(endpoint, {
      credentials: 'include',
      method: 'DELETE',
      headers: {
        Authorization: this.auth
      }
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      switch (response.status) {
        case 403:
          throw new GeoServerResponseError(
            'Deletion failed. There might be dependant layers to this style. Delete them first or call this with "recurse=false"',
            geoServerResponse
          );
        default:
          throw new GeoServerResponseError(null, geoServerResponse);
      }
    }
  }

  /**
   * Assigns a style to a layer.
   *
   * @param {String} workspaceOfLayer The name of the layer's workspace, can be undefined
   * @param {String} layerName The name of the layer to query
   * @param {String} workspaceOfStyle The workspace of the style, can be undefined
   * @param {String} styleName The name of the style
   * @param {Boolean} [isDefaultStyle=true] If the style should be the default style of the layer
   *
   * @throws Error if request fails
   */
  async assignStyleToLayer (workspaceOfLayer, layerName, workspaceOfStyle, styleName, isDefaultStyle) {
    let qualifiedName;
    if (workspaceOfLayer) {
      qualifiedName = `${workspaceOfLayer}:${layerName}`;
    } else {
      qualifiedName = layerName;
    }
    const styleBody = await this.getStyleInformation(workspaceOfStyle, styleName);

    let url;
    // we set the style as defaultStyle, unless user explicitly provides 'false'
    if (isDefaultStyle !== false) {
      url = this.url + 'layers/' + qualifiedName + '/styles?default=true';
    } else {
      url = this.url + 'layers/' + qualifiedName + '/styles';
    }

    const response = await fetch(url, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(styleBody)
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Get information about a style.
   *
   * @param {String} workspace The name of the workspace, can be undefined
   * @param {String} styleName The name of the style
   *
   * @throws Error if request fails
   *
   * @returns {Object} An object about the style or undefined if it cannot be found
   */
  async getStyleInformation (workspace, styleName) {
    let url;
    if (workspace) {
      url = this.url + 'workspaces/' + workspace + '/styles/' + styleName + '.json';
    } else {
      url = this.url + 'styles/' + styleName + '.json';
    }

    const response = await fetch(url, {
      credentials: 'include',
      method: 'GET',
      headers: {
        Authorization: this.auth
      }
    });

    if (!response.ok) {
      const grc = new AboutClient(this.url, this.auth);
      if (await grc.exists()) {
        // GeoServer exists, but requested item does not exist,  we return empty
        return;
      } else {
        // There was a general problem with GeoServer
        const geoServerResponse = await getGeoServerResponseText(response);
        throw new GeoServerResponseError(null, geoServerResponse);
      }
    }
    return response.json();
  }
}