Java tutorial
/* * MIT License * * Copyright (c) 2016 EPAM Systems * * 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 com.epam.ngb.cli.manager.command.handler.http; import static com.epam.ngb.cli.constants.MessageConstants.*; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import com.epam.ngb.cli.entity.BiologicalDataItem; import com.epam.ngb.cli.entity.BiologicalDataItemFormat; import com.epam.ngb.cli.entity.Project; import com.epam.ngb.cli.entity.RequestPayload; import com.epam.ngb.cli.entity.ResponseResult; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.epam.ngb.cli.exception.ApplicationException; import com.epam.ngb.cli.manager.JsonMapper; import com.epam.ngb.cli.manager.command.CommandConfiguration; import com.epam.ngb.cli.manager.command.ServerParameters; import com.epam.ngb.cli.manager.command.handler.simple.AbstractSimpleCommandHandler; import com.epam.ngb.cli.manager.printer.AbstractResultPrinter; import com.epam.ngb.cli.manager.request.RequestManager; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * {code {@link AbstractHTTPCommandHandler}} represents a basic class for all * commands that send HTTP requests to NGB server. This class provides methods common to * all HTTP commands for retrieving supplementary data from the server, request authorization * and printing requests results. */ public abstract class AbstractHTTPCommandHandler extends AbstractSimpleCommandHandler { /** * For serialization and deserialization objects to JSON format */ private final ObjectMapper mapper = JsonMapper.getMapper(); /** * Constant used to determine that an error occurred on the server in response to request */ protected static final String ERROR_STATUS = "ERROR"; /** * Default values for request header initialization */ private static final String APPLICATION_JSON = "application/json"; private static final String CONTENT_TYPE = "content-type"; private static final String CACHE_CONTROL = "cache-control"; private static final String CACHE_CONTROL_NO_CACHE = "no-cache"; /** * Delimiter between path to file and path to index in the input argument string */ private static final String INDEX_DELIMITER = "\\?"; /** * Parameters of a NGB server connection along with some URLs common to all HTTP command handlers */ protected ServerParameters serverParameters; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHTTPCommandHandler.class); /** * Default constructor to enable reflexion instance creation */ public AbstractHTTPCommandHandler() { //no operations } /** * Creates a new {@code {@link AbstractHTTPCommandHandler}} with a configuration of an * external {@code {@link AbstractHTTPCommandHandler}} * @param handler to copy configuration from */ protected AbstractHTTPCommandHandler(AbstractHTTPCommandHandler handler) { this.serverParameters = handler.getServerParameters(); this.configuration = new CommandConfiguration(handler.getConfiguration()); } /** * @return true is authorization is required for performing a HTTP request corresponding to * a command */ public boolean isSecure() { return configuration.isSecure(); } public ServerParameters getServerParameters() { return serverParameters; } public void setServerParameters(ServerParameters serverParameters) { this.serverParameters = serverParameters; } public String getRequestType() { return configuration.getRequestType(); } public String getRequestUrl() { return configuration.getRequestUrl(); } public void setRequestUrl(String requestUrl) { this.configuration.setRequestUrl(requestUrl); } public void setRequestType(String requestType) { this.configuration.setRequestType(requestType); } /** * Creates an empty {@code {@link HttpRequestBase}} with a specified url according to command * configuration * @param request URL * @return {@code {@link HttpRequestBase}} with a specified URL and type */ protected HttpRequestBase getRequest(String request) { String url = serverParameters.getServerUrl() + request; HttpRequestBase result; switch (getRequestType()) { case "POST": result = new HttpPost(url); break; case "GET": result = new HttpGet(url); break; case "DELETE": result = new HttpDelete(url); break; case "PUT": result = new HttpPut(url); break; default: throw new IllegalArgumentException("Unsupported request type: " + getRequestType()); } return result; } /** * Sends request to NGB server, retrieves an authorization token and adds it to an input request. * This is required for secure requests. * @param request to authorize */ protected void addAuthorizationToRequest(HttpRequestBase request) { try { HttpPost post = new HttpPost(serverParameters.getServerUrl() + serverParameters.getAuthenticationUrl()); StringEntity input = new StringEntity(serverParameters.getAuthPayload()); input.setContentType(APPLICATION_JSON); post.setEntity(input); post.setHeader(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); post.setHeader(CONTENT_TYPE, "application/x-www-form-urlencoded"); String result = RequestManager.executeRequest(post); Authentication authentication = getMapper().readValue(result, Authentication.class); request.setHeader("authorization", "Bearer " + authentication.getAccessToken()); } catch (IOException e) { throw new ApplicationException("Failed to authenticate request", e); } } /** * @return a JSON mapper for objects (de)serialization */ protected ObjectMapper getMapper() { return mapper; } /** * Adds a default header for HTTP request * @param request to add header */ protected void setDefaultHeader(HttpRequestBase request) { request.setHeader(CONTENT_TYPE, APPLICATION_JSON); request.setHeader(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); } /** * Retrieves BiologicalDataItemID for a file from an input String. * If input String might be interpreted as a number, this number will be returned as a result. * If input String isn't a number, method interprets it as a file name and tries to find a file * with such a name in the NGB server and retrieves its ID from a loaded data. * @param strId input String for file identification * @return BiologicalDataItemID of a file * @throws ApplicationException if method fails to find a file */ protected Long parseFileId(String strId) { if (NumberUtils.isDigits(strId)) { return Long.parseLong(strId); } else { List<BiologicalDataItem> items = loadItemsByName(strId); if (items == null || items.isEmpty()) { throw new ApplicationException(getMessage(ERROR_FILE_NOT_FOUND, strId)); } if (items.size() > 1) { LOGGER.error(getMessage(SEVERAL_RESULTS_FOR_QUERY, strId)); } return items.get(0).getBioDataItemId(); } } /** * Loads BiologicalDataItem file from NGB server by an input String. * If input String might be interpreted as a number, item will be loaded by BiologicalDataItemID. * If input String isn't a number, method interprets it as a file name and tries to find a file * with such a name in the NGB server. * @param strId input String for file identification * @return BiologicalDataItem representing file * @throws ApplicationException if method fails to find a file */ protected BiologicalDataItem loadFileByNameOrBioID(String strId) { if (NumberUtils.isDigits(strId)) { return loadFileByBioID(strId); } else { List<BiologicalDataItem> items = loadItemsByName(strId); if (items == null || items.isEmpty()) { throw new ApplicationException(getMessage(ERROR_FILE_NOT_FOUND, strId)); } if (items.size() > 1) { LOGGER.error(getMessage(SEVERAL_RESULTS_FOR_QUERY, strId)); } return items.get(0); } } /** * Retrieves reference ID from an input String. * If input String might be interpreted as a number, this number is assumed to be a BiologicalDataItemID * for a reference and thus a reference is loaded from the server by this BiologicalDataItemID. * If input String isn't a number, method interprets it as a file name, tries to find a reference * with such a name in the NGB server and retrieves its ID from a loaded data. * @param strId input String for reference identification * @return ID of a reference */ //TODO: remove this method and all connected logic when ID and BiologicalDataItemID are merged on the server protected Long loadReferenceId(String strId) { if (NumberUtils.isDigits(strId)) { BiologicalDataItem reference = loadFileByBioID(strId); return reference.getId(); } else { List<BiologicalDataItem> items = loadItemsByName(strId); checkLoadedItems(strId, items); return items.get(0).getId(); } } /** * Retrieves ID for a dataset(project) from an input String. * If input String might be interpreted as a number, this number will be returned as a result. * If input String isn't a number, method interprets it as a dataset name and tries to find a file * with such a name in the NGB server and retrieves its ID from a loaded data. * @param strId input String for dataset identification * @return ID of a dataset */ protected Long parseProjectId(String strId) { if (NumberUtils.isDigits(strId)) { return Long.parseLong(strId); } else { Project project = loadProjectByName(strId); return project.getId(); } } /** * Loads a reference ID for a dataset(project) specified by ID. * Method loads a dataset from NGB by ID and finds a reference in it. * @param datasetId find reference for * @return ID of a reference */ protected Long loadReferenceIdFromDataset(Long datasetId) { try { Project project = loadProject(datasetId); for (BiologicalDataItem item : project.getItems()) { if (item.getFormat() == BiologicalDataItemFormat.REFERENCE) { return item.getId(); } } throw new ApplicationException( String.format("No reference specified for dataset with id %d.", datasetId)); } catch (IOException e) { throw new ApplicationException("", e); } } /** * Finds files on the NGB server with a name matching input query. * @param strId query to find * @param strict determines type of search, if true a strict equality by name search is performed * otherwise a substring case-insensitive search is done * @return list of files matching a query */ protected List<BiologicalDataItem> loadItemsByName(String strId, boolean strict) { try { URI uri = new URIBuilder(serverParameters.getServerUrl() + serverParameters.getSearchUrl()) .addParameter("name", strId).addParameter("strict", String.valueOf(strict)).build(); HttpGet get = new HttpGet(uri); setDefaultHeader(get); String result = RequestManager.executeRequest(get); ResponseResult<List<BiologicalDataItem>> responseResult = getMapper().readValue(result, getMapper().getTypeFactory().constructParametrizedType(ResponseResult.class, ResponseResult.class, getMapper().getTypeFactory().constructParametrizedType(List.class, List.class, BiologicalDataItem.class))); if (responseResult == null) { throw new ApplicationException(getMessage(ERROR_FILE_NOT_FOUND, strId)); } return responseResult.getPayload(); } catch (IOException | URISyntaxException e) { throw new ApplicationException(e.getMessage(), e); } } /** * Performs a POST HTTP request to register some data on NGB server * @param requestObject filled request object for the server * @param post request to performs * @return response value as a String */ protected String getPostResult(RequestPayload requestObject, HttpPost post) { try { post.setEntity(new StringEntity(getMapper().writeValueAsString(requestObject))); return RequestManager.executeRequest(post); } catch (UnsupportedEncodingException | JsonProcessingException e) { throw new ApplicationException(e.getMessage(), e); } } /** * Performs an HTTP request to delete some data, specified by ID, from NGB server * @param id of the data to delete */ protected void runDeletion(Long id) { String url = String.format(getRequestUrl(), id); HttpRequestBase request = getRequest(url); setDefaultHeader(request); if (isSecure()) { addAuthorizationToRequest(request); } String result = RequestManager.executeRequest(request); try { ResponseResult response = getMapper().readValue(result, getMapper().getTypeFactory().constructType(ResponseResult.class)); LOGGER.info(response.getStatus() + "\t" + response.getMessage()); } catch (IOException e) { throw new ApplicationException(e.getMessage(), e); } } /** * Checks that file registration request completed successfully, * serializes a request result to an {@code {@link BiologicalDataItem}} object and prints it * to StdOut, id it's required by the print options * @param result of a registration result * @param printJson if true, result wil be printed in Json format * @param printTable if true, result wil be printed in table format */ protected void checkAndPrintRegistrationResult(String result, boolean printJson, boolean printTable) { try { ResponseResult<BiologicalDataItem> responseResult = getMapper().readValue(result, getMapper().getTypeFactory().constructParametrizedType(ResponseResult.class, ResponseResult.class, BiologicalDataItem.class)); if (ERROR_STATUS.equals(responseResult.getStatus())) { throw new ApplicationException(responseResult.getMessage()); } else { if (printJson || printTable) { List<BiologicalDataItem> items = Collections.singletonList(responseResult.getPayload()); AbstractResultPrinter printer = AbstractResultPrinter.getPrinter(printTable, items.get(0).getFormatString(items)); printer.printHeader(items.get(0)); items.forEach(printer::printItem); } } } catch (IOException e) { throw new ApplicationException(e.getMessage(), e); } } protected <T> void checkAndPrintResult(String result, boolean printJson, boolean printTable, Class<T> respClass) { try { ResponseResult<T> responseResult = getMapper().readValue(result, getMapper().getTypeFactory() .constructParametrizedType(ResponseResult.class, ResponseResult.class, respClass)); if (ERROR_STATUS.equals(responseResult.getStatus())) { throw new ApplicationException(responseResult.getMessage()); } else { if (printJson || printTable) { AbstractResultPrinter printer = AbstractResultPrinter.getPrinter(printTable, "%s"); printer.printSimple(responseResult.getPayload().toString()); } } } catch (IOException e) { throw new ApplicationException(e.getMessage(), e); } } /** * Checks if result contains no errors. If it does, throws {@link ApplicationException} * @param result request result */ protected void isResultOk(String result) { try { ResponseResult<Object> responseResult = getMapper().readValue(result, getMapper().getTypeFactory() .constructParametrizedType(ResponseResult.class, ResponseResult.class, Object.class)); if (ERROR_STATUS.equals(responseResult.getStatus())) { throw new ApplicationException(responseResult.getMessage()); } } catch (IOException e) { throw new ApplicationException(e.getMessage(), e); } } protected <T> T getResult(String result, Class<T> respClass) { try { ResponseResult<T> responseResult = getMapper().readValue(result, getMapper().getTypeFactory() .constructParametrizedType(ResponseResult.class, ResponseResult.class, respClass)); if (ERROR_STATUS.equals(responseResult.getStatus())) { throw new ApplicationException(responseResult.getMessage()); } else { return responseResult.getPayload(); } } catch (IOException e) { throw new ApplicationException(e.getMessage(), e); } } /** * Checks that dataset(project) registration request completed successfully, * serializes a request result to an {@code {@link Project}} object and prints it * to StdOut, id it's required by the print options * @param result of a registration result * @param printJson if true, result wil be printed in Json format * @param printTable if true, result wil be printed in table format */ protected void checkAndPrintDatasetResult(String result, boolean printJson, boolean printTable) { try { ResponseResult<Project> responseResult = getMapper().readValue(result, getMapper().getTypeFactory() .constructParametrizedType(ResponseResult.class, ResponseResult.class, Project.class)); if (ERROR_STATUS.equals(responseResult.getStatus())) { throw new ApplicationException(responseResult.getMessage()); } else { if (printJson || printTable) { List<Project> items = Collections.singletonList(responseResult.getPayload()); AbstractResultPrinter printer = AbstractResultPrinter.getPrinter(printTable, items.get(0).getFormatString(items)); printer.printHeader(items.get(0)); items.forEach(printer::printItem); } } } catch (IOException e) { throw new ApplicationException(e.getMessage(), e); } } /** * Performs a request to add or remove files from a dataset on NGB server * @param items to add or remove from a dataset * @param datasetId specifies dataset * @param printJson if true, result wil be printed in Json format * @param printTable if true, result wil be printed in table format */ protected void addOrDeleteItemsFromDataset(List<Long> items, Long datasetId, boolean printJson, boolean printTable) { String result = null; for (Long id : items) { String url = String.format(getRequestUrl(), datasetId, id); HttpRequestBase request = getRequest(url); setDefaultHeader(request); if (isSecure()) { addAuthorizationToRequest(request); } result = RequestManager.executeRequest(request); } if (result != null) { checkAndPrintDatasetResult(result, printJson, printTable); } } protected Project loadProject(Long datasetId) throws IOException { HttpGet get = new HttpGet(serverParameters.getServerUrl() + String.format(serverParameters.getProjectLoadByIdUrl(), datasetId)); setDefaultHeader(get); String result = RequestManager.executeRequest(get); ResponseResult<Project> responseResult = getMapper().readValue(result, getMapper().getTypeFactory() .constructParametrizedType(ResponseResult.class, ResponseResult.class, Project.class)); if (responseResult == null || responseResult.getPayload() == null) { throw new ApplicationException(getMessage(ERROR_PROJECT_NOT_FOUND, datasetId)); } return responseResult.getPayload(); } private BiologicalDataItem loadFileByBioID(String id) { try { URI uri = new URIBuilder(serverParameters.getServerUrl() + serverParameters.getFileFindUrl()) .addParameter("id", id).build(); HttpGet get = new HttpGet(uri); setDefaultHeader(get); String result = RequestManager.executeRequest(get); ResponseResult<BiologicalDataItem> responseResult = getMapper().readValue(result, getMapper().getTypeFactory().constructParametrizedType(ResponseResult.class, ResponseResult.class, BiologicalDataItem.class)); if (responseResult == null || responseResult.getPayload() == null) { throw new ApplicationException("Failed to find a file by Bio item ID: " + id + "."); } return responseResult.getPayload(); } catch (IOException | URISyntaxException e) { throw new ApplicationException("", e); } } private Project loadProjectByName(String strId) { try { URI uri = new URIBuilder(serverParameters.getServerUrl() + serverParameters.getProjectLoadUrl()) .addParameter("projectName", strId).build(); HttpGet get = new HttpGet(uri); setDefaultHeader(get); String result = RequestManager.executeRequest(get); ResponseResult<Project> responseResult = getMapper().readValue(result, getMapper().getTypeFactory() .constructParametrizedType(ResponseResult.class, ResponseResult.class, Project.class)); if (responseResult == null || responseResult.getPayload() == null) { throw new ApplicationException("Failed to find a project by name: " + strId + "."); } return responseResult.getPayload(); } catch (IOException | URISyntaxException e) { throw new ApplicationException("", e); } } private List<BiologicalDataItem> loadItemsByName(String strId) { return loadItemsByName(strId, true); } private void checkLoadedItems(String strId, List<BiologicalDataItem> items) { if (items == null || items.isEmpty()) { throw new ApplicationException(getMessage(ERROR_REFERENCE_NOT_FOUND, strId)); } if (items.size() > 1) { LOGGER.info(getMessage(SEVERAL_RESULTS_FOR_QUERY, strId)); } if (items.get(0).getFormat() != BiologicalDataItemFormat.REFERENCE) { throw new ApplicationException(getMessage(ERROR_REFERENCE_NOT_FOUND, strId)); } } /** * Method splits path for file registartion into file-index pair * @param path input CLI argument * @return pair, where left part is path to file and rights one optional path to index */ protected Pair<String, String> parseAndVerifyFilePath(String path) { Pair<String, String> fileWithIndex = splitFilePath(path); BiologicalDataItemFormat format = BiologicalDataItemFormat.getByFilePath(fileWithIndex.getLeft()); if (format.isRequireIndex() && fileWithIndex.getRight() == null) { throw new IllegalArgumentException(getMessage(ERROR_INDEX_REQUIRED, fileWithIndex.getLeft())); } if (fileWithIndex.getRight() != null) { boolean indexSupported = format.verifyIndex(fileWithIndex.getRight()); //if server doesn't support a given index, but index is also not required //we don't pass it to server if (!indexSupported) { return new ImmutablePair<>(fileWithIndex.getLeft(), null); } } return fileWithIndex; } private Pair<String, String> splitFilePath(String path) { String[] paths = path.split(INDEX_DELIMITER); if (paths.length > 2) { throw new IllegalArgumentException(getMessage(ILLEGAL_PATH_FORMAT, path)); } if (paths.length == 1) { return new ImmutablePair<>(paths[0], null); } else { return new ImmutablePair<>(paths[0], paths[1]); } } /** * Represents a result of authorization request to NGB server */ public static class Authentication { /** * Token for secure requests authorization */ @JsonProperty("access_token") private String accessToken; public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } } }