content: /*
   ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
   ~                                                                               ~
   ~ The MIT License (MIT)                                                         ~
   ~                                                                               ~
   ~ Copyright (c) 2015-2025 miaixz.org gitlab4j and other contributors.           ~
   ~                                                                               ~
   ~ Permission is hereby granted, free of charge, to any person obtaining a copy  ~
   ~ of this software and associated documentation files (the "Software"), to deal ~
   ~ in the Software without restriction, including without limitation the rights  ~
   ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell     ~
   ~ copies of the Software, and to permit persons to whom the Software is         ~
   ~ furnished to do so, subject to the following conditions:                      ~
   ~                                                                               ~
   ~ The above copyright notice and this permission notice shall be included in    ~
   ~ all copies or substantial portions of the Software.                           ~
   ~                                                                               ~
   ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR    ~
   ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,      ~
   ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE   ~
   ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER        ~
   ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ~
   ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN     ~
   ~ THE SOFTWARE.                                                                 ~
   ~                                                                               ~
   ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
  */
  package org.miaixz.bus.gitlab.support;
  
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.net.HttpURLConnection;
  import java.net.URL;
  import java.net.URLConnection;
  import java.net.URLEncoder;
  import java.util.Arrays;
  import java.util.List;
  import java.util.StringJoiner;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import org.miaixz.bus.gitlab.GitLabApiException;
  
  import com.fasterxml.jackson.annotation.JsonCreator;
  import com.fasterxml.jackson.annotation.JsonValue;
  
  /**
   * This class uses HTML scraping to create and revoke GitLab personal access tokens, the user's Feed token, and for
   * fetching the current Health Check access token.
   *
   * <p>
   * NOTE: This relies on HTML scraping and has been tested on GitLab-CE 11.0.0 to 11.10.1 for proper functionality. It
   * may not work on earlier or later versions.
   * </p>
   */
  public final class AccessToken {
  
      /**
       * This enum defines the available scopes for a personal access token.
       */
      public enum Scope {
  
          /**
           * Grants complete access to the API and Container Registry (read/write) (introduced in GitLab 8.15).
           */
          API,
  
          /**
           * Allows to read (pull) container registry images if a project is private and authorization is required
           * (introduced in GitLab 9.3). If the GitLab server you are using does not have the Registry properly
           * configured, using this scope will result in an exception.
           */
          READ_REGISTRY,
  
          /**
           * Allows read-only access (pull) to the repository through git clone.
           */
          READ_REPOSITORY,
  
          /**
           * Allows access to the read-only endpoints under /users. Essentially, any of the GET requests in the Users API
           * are allowed (introduced in GitLab 8.15).
           */
          READ_USER,
  
          /**
           * Allows performing API actions as any user in the system, if the authenticated user is an admin (introduced in
           * GitLab 10.2).
           */
          SUDO,
  
          /**
           * Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API).
           */
          WRITE_REPOSITORY;
  
          private static JacksonJsonEnumHelper<Scope> enumHelper = new JacksonJsonEnumHelper<>(Scope.class);
  
          @JsonCreator
          public static Scope forValue(String value) {
              return enumHelper.forValue(value);
          }
  
          @JsonValue
          public String toValue() {
              return (enumHelper.toString(this));
          }
  
          @Override
          public String toString() {
              return (enumHelper.toString(this));
          }
      }
  
      protected static final String USER_AGENT = "GitLab4J Client";
      protected static final String COOKIES_HEADER = "Set-Cookie";
  
      protected static final String NEW_USER_AUTHENTICITY_TOKEN_REGEX = "\"new_user\".*name=\\\"authenticity_token\\\"\\svalue=\\\"([^\\\"]*)\\\".*new_new_user";
      protected static final Pattern NEW_USER_AUTHENTICITY_TOKEN_PATTERN = Pattern
              .compile(NEW_USER_AUTHENTICITY_TOKEN_REGEX);
  
      protected static final String AUTHENTICITY_TOKEN_REGEX = "name=\\\"authenticity_token\\\"\\svalue=\\\"([^\\\"]*)\\\"";
      protected static final Pattern AUTHENTICITY_TOKEN_PATTERN = Pattern.compile(AUTHENTICITY_TOKEN_REGEX);
  
      protected static final String PERSONAL_ACCESS_TOKEN_REGEX = "name=\\\"created-personal-access-token\\\".*data-clipboard-text=\\\"([^\\\"]*)\\\".*\\/>";
      protected static final Pattern PERSONAL_ACCESS_TOKEN_PATTERN = Pattern.compile(PERSONAL_ACCESS_TOKEN_REGEX);
  
      protected static final String REVOKE_PERSONAL_ACCESS_TOKEN_REGEX = "href=\\\"([^\\\"]*)\\\"";
      protected static final Pattern REVOKE_PERSONAL_ACCESS_TOKEN_PATTERN = Pattern
              .compile(REVOKE_PERSONAL_ACCESS_TOKEN_REGEX);
  
      protected static final String FEED_TOKEN_REGEX = "name=\\\"feed_token\\\".*value=\\\"([^\\\"]*)\\\".*\\/>";
      protected static final Pattern FEED_TOKEN_PATTERN = Pattern.compile(FEED_TOKEN_REGEX);
  
      protected static final String HEALTH_CHECK_ACCESS_TOKEN_REGEX = "id=\"health-check-token\">([^<]*)<\\/code>";
      protected static final Pattern HEALTH_CHECK_ACCESS_TOKEN_PATTERN = Pattern.compile(HEALTH_CHECK_ACCESS_TOKEN_REGEX);
  
      /**
       * Create a GitLab personal access token with the provided configuration.
       *
       * @param baseUrl   the GitLab server base URL
       * @param username  the user name to create the personal access token for
       * @param password  the password of the user to create the personal access token for
       * @param tokenName the name for the new personal access token
       * @param scopes    an array of scopes for the new personal access token
       * @return the created personal access token
       * @throws GitLabApiException if any exception occurs
       */
      public static final String createPersonalAccessToken(
              final String baseUrl,
              final String username,
              final String password,
              final String tokenName,
              final Scope[] scopes) throws GitLabApiException {
  
          if (scopes == null || scopes.length == 0) {
              throw new RuntimeException("scopes cannot be null or empty");
          }
  
          return (createPersonalAccessToken(baseUrl, username, password, tokenName, Arrays.asList(scopes)));
      }
  
      /**
       * Create a GitLab personal access token with the provided configuration.
       *
       * @param baseUrl   the GitLab server base URL
       * @param username  the user name to create the personal access token for
       * @param password  the password of the user to create the personal access token for
       * @param tokenName the name for the new personal access token
       * @param scopes    a List of scopes for the new personal access token
       * @return the created personal access token
       * @throws GitLabApiException if any exception occurs
       */
      public static final String createPersonalAccessToken(
              final String baseUrl,
              final String username,
              final String password,
              final String tokenName,
              final List<Scope> scopes) throws GitLabApiException {
  
          // Save the follow redirect state so it can be restored later
          boolean savedFollowRedirects = HttpURLConnection.getFollowRedirects();
          String cookies = null;
  
          try {
  
              // Must manually follow redirects
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(false);
              }
  
              /*******************************************************************************
               * Step 1: Login and get the session cookie. *
               *******************************************************************************/
              cookies = login(baseUrl, username, password);
  
              /*******************************************************************************
               * Step 2: Go to the /profile/personal_access_tokens page to fetch a * new authenticity token. *
               *******************************************************************************/
              String urlString = baseUrl + "/profile/personal_access_tokens";
              URL url = new URL(urlString);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure the response code is 200, otherwise there is a failure
              int responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Failure loading Access Tokens page, aborting!");
              }
  
              String content = getContent(connection);
              Matcher matcher = AUTHENTICITY_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("authenticity_token not found, aborting!");
              }
  
              String csrfToken = matcher.group(1);
  
              /*******************************************************************************
               * Step 3: Submit the /profile/personalccess_tokens page with the info to * create a new personal access
               * token. *
               *******************************************************************************/
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
              connection.setRequestProperty("Charset", "utf-8");
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
              connection.setRequestMethod("POST");
              connection.setDoInput(true);
              connection.setDoOutput(true);
  
              StringBuilder formData = new StringBuilder();
              addFormData(formData, "authenticity_token", csrfToken);
              addFormData(formData, "personal_access_token[name]", tokenName);
              addFormData(formData, "personal_access_token[expires_at]", "");
  
              if (scopes != null && scopes.size() > 0) {
                  for (Scope scope : scopes) {
                      addFormData(formData, "personal_access_token[scopes][]", scope.toString());
                  }
              }
  
              connection.setRequestProperty("Content-Length", String.valueOf(formData.length()));
              OutputStream output = connection.getOutputStream();
              output.write(formData.toString().getBytes());
              output.flush();
              output.close();
  
              // Make sure a redirect was provided, otherwise there is a failure
              responseCode = connection.getResponseCode();
              if (responseCode != 302) {
                  throw new GitLabApiException("Failure creating personal access token, aborting!");
              }
  
              // Follow the redirect with the provided session cookie
              String redirectUrl = connection.getHeaderField("Location");
              url = new URL(redirectUrl);
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure the response code is 200, otherwise there is a failure
              responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Failure creating personal access token, aborting!");
              }
  
              // Extract the personal access token from the page and return it
              content = getContent(connection);
              matcher = PERSONAL_ACCESS_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("created-personal-access-token not found, aborting!");
              }
  
              String personalAccessToken = matcher.group(1);
              return (personalAccessToken);
  
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          } finally {
  
              if (cookies != null) {
                  try {
                      logout(baseUrl, cookies);
                  } catch (Exception ignore) {
                  }
              }
  
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(true);
              }
          }
      }
  
      /**
       * Revoke the first matching GitLab personal access token.
       *
       * @param baseUrl   the GitLab server base URL
       * @param username  the user name to revoke the personal access token for
       * @param password  the password of the user to revoke the personal access token for
       * @param tokenName the name of the personal access token to revoke
       * @param scopes    an array of scopes of the personal access token to revoke
       * @throws GitLabApiException if any exception occurs
       */
      public static final void revokePersonalAccessToken(
              final String baseUrl,
              final String username,
              final String password,
              final String tokenName,
              final Scope[] scopes) throws GitLabApiException {
  
          if (scopes == null || scopes.length == 0) {
              throw new RuntimeException("scopes cannot be null or empty");
          }
  
          revokePersonalAccessToken(baseUrl, username, password, tokenName, Arrays.asList(scopes));
      }
  
      /**
       * Revoke the first matching GitLab personal access token.
       *
       * @param baseUrl   the GitLab server base URL
       * @param username  the user name to revoke the personal access token for
       * @param password  the password of the user to revoke the personal access token for
       * @param tokenName the name of the personal access token to revoke
       * @param scopes    a List of scopes of the personal access token to revoke
       * @throws GitLabApiException if any exception occurs
       */
      public static final void revokePersonalAccessToken(
              final String baseUrl,
              final String username,
              final String password,
              final String tokenName,
              final List<Scope> scopes) throws GitLabApiException {
  
          // Save the follow redirect state so it can be restored later
          boolean savedFollowRedirects = HttpURLConnection.getFollowRedirects();
          String cookies = null;
  
          try {
  
              // Must manually follow redirects
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(false);
              }
  
              /*******************************************************************************
               * Step 1: Login and get the session cookie. *
               *******************************************************************************/
              cookies = login(baseUrl, username, password);
  
              /*******************************************************************************
               * Step 2: Go to the /profile/personal_access_tokens page and fetch the * authenticity token. *
               *******************************************************************************/
              String urlString = baseUrl + "/profile/personal_access_tokens";
              URL url = new URL(urlString);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure the response code is 200, otherwise there is a failure
              int responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Failure loading Access Tokens page, aborting!");
              }
  
              String content = getContent(connection);
              Matcher matcher = AUTHENTICITY_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("authenticity_token not found, aborting!");
              }
  
              String csrfToken = matcher.group(1);
  
              /*******************************************************************************
               * Step 3: Submit the /profile/personal_access_tokens page with the info to * revoke the first matching
               * personal access token. *
               *******************************************************************************/
              int indexOfTokenName = content.indexOf("<td>" + tokenName + "</td>");
              if (indexOfTokenName == -1) {
                  throw new GitLabApiException("personal access token not found, aborting!");
              }
  
              content = content.substring(indexOfTokenName);
              int indexOfLinkEnd = content.indexOf("</a>");
              if (indexOfTokenName == -1) {
                  throw new GitLabApiException("personal access token not found, aborting!");
              }
  
              content = content.substring(0, indexOfLinkEnd);
              String scopesText = "";
              if (scopes != null && scopes.size() > 0) {
                  final StringJoiner joiner = new StringJoiner(", ");
                  scopes.forEach(s -> joiner.add(s.toString()));
                  scopesText = joiner.toString();
              }
  
              if (content.indexOf(scopesText) == -1) {
                  throw new GitLabApiException("personal access token not found, aborting!");
              }
  
              matcher = REVOKE_PERSONAL_ACCESS_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("personal access token not found, aborting!");
              }
  
              String revokePath = matcher.group(1);
              url = new URL(baseUrl + revokePath);
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
              connection.setRequestProperty("Charset", "utf-8");
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
              connection.setRequestMethod("PUT");
              connection.setDoInput(true);
              connection.setDoOutput(true);
  
              // Submit the form
              StringBuilder formData = new StringBuilder();
              addFormData(formData, "authenticity_token", csrfToken);
              connection.setRequestProperty("Content-Length", String.valueOf(formData.length()));
              OutputStream output = connection.getOutputStream();
              output.write(formData.toString().getBytes());
              output.flush();
              output.close();
  
              // Make sure a redirect was provided, otherwise there is a failure
              responseCode = connection.getResponseCode();
              if (responseCode != 302) {
                  throw new GitLabApiException("Failure revoking personal access token, aborting!");
              }
  
              // Follow the redirect with the provided session cookie
              String redirectUrl = connection.getHeaderField("Location");
              url = new URL(redirectUrl);
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure the response code is 200, otherwise there is a failure
              responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Failure revoking personal access token, aborting!");
              }
  
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          } finally {
  
              if (cookies != null) {
                  try {
                      logout(baseUrl, cookies);
                  } catch (Exception ignore) {
                  }
              }
  
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(true);
              }
          }
      }
  
      /**
       * Fetches the user's GitLab Feed token using HTML scraping.
       *
       * @param baseUrl  the GitLab server base URL
       * @param username the user name the user to log in with
       * @param password the password of the provided username
       * @return the fetched Feed token
       * @throws GitLabApiException if any exception occurs
       */
      public static final String getFeedToken(final String baseUrl, final String username, final String password)
              throws GitLabApiException {
  
          // Save the follow redirect state so it can be restored later
          boolean savedFollowRedirects = HttpURLConnection.getFollowRedirects();
          String cookies = null;
  
          try {
  
              // Must manually follow redirects
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(false);
              }
  
              /*******************************************************************************
               * Step 1: Login and get the session cookie. *
               *******************************************************************************/
              cookies = login(baseUrl, username, password);
  
              /*******************************************************************************
               * Step 2: Go to the /profile/personal_access_tokens page and fetch the * Feed token. *
               *******************************************************************************/
              String urlString = baseUrl + "/profile/personal_access_tokens";
              URL url = new URL(urlString);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure the response code is 200, otherwise there is a failure
              int responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Failure loading Access Tokens page, aborting!");
              }
  
              // Extract the Feed token from the page and return it
              String content = getContent(connection);
              Matcher matcher = FEED_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("Feed token not found, aborting!");
              }
  
              String feedToken = matcher.group(1);
              return (feedToken);
  
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          } finally {
  
              if (cookies != null) {
                  try {
                      logout(baseUrl, cookies);
                  } catch (Exception ignore) {
                  }
              }
  
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(true);
              }
          }
      }
  
      /**
       * Fetches the GitLab health check access token using HTML scraping.
       *
       * @param baseUrl  the GitLab server base URL
       * @param username the user name of an admin user to log in with
       * @param password the password of the provided username
       * @return the fetched health check access token
       * @throws GitLabApiException if any exception occurs
       */
      public static final String getHealthCheckAccessToken(
              final String baseUrl,
              final String username,
              final String password) throws GitLabApiException {
  
          // Save the follow redirect state so it can be restored later
          boolean savedFollowRedirects = HttpURLConnection.getFollowRedirects();
          String cookies = null;
  
          try {
  
              // Must manually follow redirects
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(false);
              }
  
              /*******************************************************************************
               * Step 1: Login and get the session cookie. *
               *******************************************************************************/
              cookies = login(baseUrl, username, password);
  
              /*******************************************************************************
               * Step 2: Go to the /admin/health_check page and fetch the * health check access token. *
               *******************************************************************************/
              String urlString = baseUrl + "/admin/health_check";
              URL url = new URL(urlString);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure the response code is 200, otherwise there is a failure
              int responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Failure loading Health Check page, aborting!");
              }
  
              // Extract the personal access token from the page and return it
              String content = getContent(connection);
              Matcher matcher = HEALTH_CHECK_ACCESS_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("health-check-access-token not found, aborting!");
              }
  
              String healthCheckAccessToken = matcher.group(1);
              return (healthCheckAccessToken);
  
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          } finally {
  
              if (cookies != null) {
                  try {
                      logout(baseUrl, cookies);
                  } catch (Exception ignore) {
                  }
              }
  
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(true);
              }
          }
      }
  
      /**
       * Gets a GitLab session cookie by logging in the specified user.
       *
       * @param baseUrl  the GitLab server base URL
       * @param username the user name to to login for
       * @param password the password of the user to login for
       * @return the GitLab seesion token as a cookie value
       * @throws GitLabApiException if any error occurs
       */
      protected static final String login(final String baseUrl, final String username, final String password)
              throws GitLabApiException {
  
          // Save the follow redirect state so it can be restored later
          boolean savedFollowRedirects = HttpURLConnection.getFollowRedirects();
  
          try {
  
              // Must manually follow redirects
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(false);
              }
  
              /*******************************************************************************
               * Step 1: Go to the login page to get a session cookie and an athuicity_token *
               *******************************************************************************/
              String urlString = baseUrl + "/users/sign_in";
              URL url = new URL(urlString);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              connection.setRequestMethod("GET");
              connection.addRequestProperty("User-Agent", USER_AGENT);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure a redirect was provided, otherwise it is a login failure
              int responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Invalid state, aborting!");
              }
  
              // Get the session cookie from the headers
              String[] cookieParts = connection.getHeaderField(COOKIES_HEADER).split(";");
              String cookies = cookieParts[0];
  
              // Extract the authenticity token from the content, need this to submit the
              // login form
              String content = getContent(connection);
              Matcher matcher = NEW_USER_AUTHENTICITY_TOKEN_PATTERN.matcher(content);
              if (!matcher.find()) {
                  throw new GitLabApiException("authenticity_token not found, aborting!");
              }
  
              String csrfToken = matcher.group(1);
  
              /*******************************************************************************
               * Step 2: Submit the login form wih the session cookie and authenticity_token * fetching a new session
               * cookie along the way. *
               *******************************************************************************/
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
              connection.setRequestProperty("Charset", "utf-8");
  
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
              connection.setRequestMethod("POST");
              connection.setDoInput(true);
              connection.setDoOutput(true);
  
              StringBuilder formData = new StringBuilder();
              addFormData(formData, "user[login]", username);
              addFormData(formData, "user[password]", password);
              addFormData(formData, "authenticity_token", csrfToken);
              connection.setRequestProperty("Content-Length", String.valueOf(formData.length()));
  
              OutputStream output = connection.getOutputStream();
              output.write(formData.toString().getBytes());
              output.flush();
              output.close();
  
              // Make sure a redirect was provided, otherwise it is a login failure
              responseCode = connection.getResponseCode();
              if (responseCode != 302) {
                  throw new GitLabApiException("Login failure, aborting!", 401);
              }
  
              cookieParts = connection.getHeaderField(COOKIES_HEADER).split(";");
              cookies = cookieParts[0];
  
              // Follow the redirect with the provided session cookie
              String redirectUrl = connection.getHeaderField("Location");
              url = new URL(redirectUrl);
              connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // The response code should be 200, otherwise something is wrong, consider it a login failure
              responseCode = connection.getResponseCode();
              if (responseCode != 200) {
                  throw new GitLabApiException("Login failure, aborting!", 401);
              }
  
              return (cookies);
  
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          } finally {
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(true);
              }
          }
      }
  
      /**
       * Logs out the user associated with the GitLab session cookie.
       *
       * @param baseUrl the GitLab server base URL
       * @param cookies the GitLab session cookie to logout
       * @throws GitLabApiException if any error occurs
       */
      protected static final void logout(final String baseUrl, final String cookies) throws GitLabApiException {
  
          // Save so it can be restored later
          boolean savedFollowRedirects = HttpURLConnection.getFollowRedirects();
  
          try {
  
              // Must manually follow redirects
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(false);
              }
  
              String urlString = baseUrl + "/users/sign_out";
              URL url = new URL(urlString);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              connection.setRequestProperty("User-Agent", USER_AGENT);
              connection.setRequestProperty("Cookie", cookies);
              connection.setRequestMethod("GET");
              connection.setReadTimeout(10000);
              connection.setConnectTimeout(10000);
  
              // Make sure a redirect was provided, otherwise it is a logout failure
              int responseCode = connection.getResponseCode();
              if (responseCode != 302) {
                  throw new GitLabApiException("Logout failure, aborting!");
              }
  
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          } finally {
              if (savedFollowRedirects) {
                  HttpURLConnection.setFollowRedirects(true);
              }
          }
      }
  
      /**
       * Adds the specified form param to the form data StringBuilder. If the provided formData is null, will create the
       * StringBuilder instance first.
       *
       * @param formData the StringBuilder instance holding the form data, if null will create the StringBuilder
       * @param name     the form param name
       * @param value    the form param value
       * @return the form data StringBuilder
       * @throws GitLabApiException if any error occurs.
       */
      public static final StringBuilder addFormData(StringBuilder formData, String name, String value)
              throws GitLabApiException {
  
          if (formData == null) {
              formData = new StringBuilder();
          } else if (formData.length() > 0) {
              formData.append("&");
          }
  
          formData.append(name);
          formData.append("=");
          try {
              formData.append(URLEncoder.encode(value, "UTF-8"));
              return (formData);
          } catch (Exception e) {
              throw new GitLabApiException(e);
          }
      }
  
      /**
       * Reads and returns the content from the provided URLConnection.
       *
       * @param connection the URLConnection to read the content from
       * @return the read content as a String
       * @throws GitLabApiException if any error occurs
       */
      protected static String getContent(URLConnection connection) throws GitLabApiException {
  
          StringBuilder buf = new StringBuilder();
  
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
              reader.lines().forEach(b -> buf.append(b));
          } catch (IOException ioe) {
              throw new GitLabApiException(ioe);
          }
  
          return (buf.toString());
      }
  
  }
  