Package org.dtk.resources

Source Code of org.dtk.resources.Build

package org.dtk.resources;

import java.io.IOException;
import java.net.MalformedURLException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.http.HttpStatus;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.dtk.resources.build.BuildRequest;
import org.dtk.resources.build.manager.BuildState;
import org.dtk.resources.build.manager.BuildStatusManager;
import org.dtk.resources.exceptions.ConfigurationException;
import org.dtk.resources.exceptions.IncorrectParameterException;
import org.dtk.resources.exceptions.MissingResourceException;
import org.dtk.resources.packages.PackageRepository;
import org.dtk.util.FileUtil;
import org.dtk.util.HttpUtil;
import org.dtk.util.JsonUtil;

/**
* RESTful Build API. This API provides access to the package build system,
* allowing a user to generate custom builds by specifying a selection of
* modules to combine into distinct layers. The compiler build options can
* be specified here, including cross-domain builds and optimisation levels.
*
* @author James Thomas
*/

@Path("/build")
public class Build {
  @Context ServletConfig config;

  protected static final String tempBuildPrefix = "dojo";

  protected static final String tempBuildSuffix = ".temp";

  /** Base Dojo layer, must be present in any build request */
  protected static final Map<String, Object> defaultDojoLayer = new HashMap<String, Object> () {{
    put("name", "dojo.js");
    put("modules", new ArrayList<String>());
  }};
 
  /** Response messages **/
  /** Error text when build request misses mandatory parameter */
  protected static final String missingParameterErrorText
  = "Missing mandatory parameter, %1$s, from build request.";

  /** Error text when build request misses dojo package details */
  protected static final String invalidPackageErrorText
  = "Invalid package reference, %1$s, in the build request.";
 
  /** Error text when build request misses dojo package details */
  protected static final String invalidModulePackageErrorText
  = "Invalid package reference for module, %1$s, in the build request.";
 
  /** Error text when build request misses mandatory parameter */
  protected static final String urlConstructionErrorText
  = "Internal server error generating absolute resource path. Please try again.";
 
  /** Error text when json mapper fails converting layers parameters */
  protected static final String layersParsingErrorText
  = "Unable to parse layers parameter, check request contents.";
 
  /** Error text when server doesn't have SHA-1 hash algo available */
  protected static final String missingAlgorithmErrorText
  = "Failed to create build request, issues accessing hashing algorithm SHA-1";
 
  /** Error text when user has requested a build result that isn't available */
  protected static final String missingBuildResourceErrorText
  = "Unable to access build result, build process hasn't completed.";
 
  /** Log messages **/
  /** We have successfully parsed a user's new build request */
  protected static final String newBuildRequestLogMsg
  = "New build request submitted using request object: %1$s";

  /** Fatal error caught trying to process user's build request. Log all details. **/
  protected static final String fatalBuildRequestLogMsg
  = "Fatal error occurred processing the following build request, %1$s. " +
    "The following exceptions was caught: %2$s"
 
  /** Logging class instance */
  protected static final Logger logger = Logger.getLogger(Build.class.getName());
 
  /** Access to the package repository */
  protected PackageRepository repo = PackageRepository.getInstance();   
 
  /**
   * Initiate a build request, passing parameters to the Dojo build system, generating
   * a compressed version of the requested JavaScript layers. User will have a link
   * to the status polling API returned, which allows them to verify a build has finished
   * and download the result. Incorrect and/or missing parameters will generate appropriate
   * JavaScript error response
   *
   * @param request - Http request details
   * @param buildDetails - Json object request details.
   * @return Build result details, contains link to status checking resource.
   */
  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  public Response  generateBuild(@Context HttpServletRequest request, HashMap<String, Object> buildDetails) {
    logger.entering(this.getClass().getName(), "generateBuild");
   
    // We need to generate a non-standard HTTP response, 201.
    Response accepted = null;
   
    BuildRequest buildRequest = generateNewBuildRequest(buildDetails);
   
    // Schedule build request with status manager
    BuildStatusManager buildStatusManager = BuildStatusManager.getInstance();
    buildStatusManager.scheduleBuildRequest(buildRequest);
   
    // Access unique identifier for these build parameters
    String buildResultId = buildRequest.getBuildReference();
   
    // Store full URL reference for build status link, allowing client to navigate
    // to resource verifying build status.
    HashMap<String, Object> buildResponse = new HashMap<String, Object>();
    buildResponse.put("buildStatusLink", buildRequestStatusPath(request, buildResultId));

    // HTTP 202, request accepted for processing, with JSON content response.
    accepted = Response.status(HttpStatus.SC_ACCEPTED).entity(buildResponse).build();

    // TODO: Do we want this? If user clicks to build for a second time after making
    // some changes they will get an error because their app is gone.
    // Clean up temporary directory used for user application now build has finished.
    //if (userAppPath != null) {
    //  FileUtil.deleteDirectory(new File(userAppPath));
    ///}   

    logger.exiting(this.getClass().getName(), "generateBuild");
   
    return accepted;
  }

  /**
   * Return build status for a particular build reference.
   * Builds can either be completing or building. Builds
   * not completed, contain the latest build results. Builds
   * finished, have a link to the finished resource.
   *
   * @param request - Http Request
   * @param ref - Build Reference Identifier
   * @return Build job details
   */
  @GET
  @Path("status/{ref}")
  @Produces(MediaType.APPLICATION_JSON)
  public HashMap<String, String> retrieveBuildStatus(@Context HttpServletRequest request, @Context HttpServletResponse response,
    @PathParam("ref") String reference) {
    logger.entering(this.getClass().getName(), "retrieveBuildStatus");
   
    HashMap<String, String> buildStatus = new HashMap<String, String>();

    BuildStatusManager buildStateManager = BuildStatusManager.getInstance();
   
    // Retrieve current build state for reference build
    BuildState buildState = buildStateManager.retrieveBuildState(reference);
   
    // Store resulting state in the response
    buildStatus.put("state", buildState.toString());
    buildStatus.put("logs", buildStateManager.getCompleteBuildLog(reference));
    // If build has completed, include resource link, otherwise just include
    // current logs.
    if (buildState == BuildState.COMPLETED) {
      buildStatus.put("result", layersBuildResourcePath(request, reference));
    // Unless build is complete, response contents will likely change so ensure result isn't
    // cached. I'm looking at you Internet Explorer...
    } else {
      response.addHeader("Cache-Control", "no-cache, must-revalidate");
      response.addHeader("Pragma", "no-cache");
    }
   
    logger.exiting(this.getClass().getName(), "retrieveBuildStatus");   
    return buildStatus;
  }

  /**
   * Retrieve the build result for a given reference.
   * Invalid resources result in a 404 response.
   *
   * @param response - HTTP Response
   * @param reference - Build resource identifier
   * @return Output stream for compressed Dojo build.
   */
  @GET
  @Path("{reference}")
  @Produces("application/zip")
  public StreamingOutput retrieveBuildResult(@Context HttpServletResponse response, @PathParam("reference") String reference) {
    logger.entering(this.getClass().getName(), "retrieveBuildResult");
    BuildStatusManager buildStateManager = BuildStatusManager.getInstance();
   
    // Retrieve current build state for reference build
    BuildState buildState = buildStateManager.retrieveBuildState(reference);
   
    if (buildState != BuildState.COMPLETED) {
      throw new MissingResourceException(missingBuildResourceErrorText);
    }

    // Set header to force download of content rather than display.
    response.setHeader(HttpUtil.contentDisposition, HttpUtil.contentDispositionAttachment);
   
    logger.exiting(this.getClass().getName(), "retrieveBuildResult");     
    // Stream file output back to the user
    return FileUtil.streamingFileOutput(buildStateManager.getBuildResultPath(reference), false);
  }

  /**
   * Construct a new instance of the BuildRequest object. Extract mandatory
   * parameters from the request map, verifying that parameters are valid,
   * before passing into new build request instance.
   *
   * @param buildDetails - JSON request parameters
   * @return New build request instance
   * @throws IncorrectParameterException - User error in request details
   * @throws ConfigurationException - Internal server error occurred.
   */
  protected BuildRequest generateNewBuildRequest(Map<String, Object> buildDetails)
  throws IncorrectParameterException, ConfigurationException {
    // Retrieve mandatory package descriptions, must contain Dojo package
    List<Map<String, String>> packages = (List<Map<String, String>>) extractBuildPackages(buildDetails);
   
    // Retrieve standard mandatory and optional build parameters from JSON request.
    String cdn = (String) extractMandatoryParameter(buildDetails, "cdn"),
      optimise = (String) extractMandatoryParameter(buildDetails, "optimise"),
      cssOptimise = (String) extractMandatoryParameter(buildDetails, "cssOptimise"),
      platforms = (String) extractMandatoryParameter(buildDetails, "platforms"),
      themes = (String) extractMandatoryParameter(buildDetails, "themes");

    // Construct list of reference package identiers
    Set<String> packageIds = new HashSet<String>();
    for(Map<String, String> packageRef: packages) {
      packageIds.add(packageRef.get("name"));
    }
   
    // Extract additional build layers, checking any module dependencies reference valid
    // packages.
    List<Map<String, Object>> layers = extractBuildLayers(buildDetails, packageIds);   
   
    // Instantiate new build request with user parameters, catch construction exceptions
    // and throw extended WebApplicationException.
    BuildRequest buildRequest;
    try {
      buildRequest = new BuildRequest(packages, cdn, optimise, cssOptimise, platforms, themes, layers);
      logger.log(Level.INFO, String.format(newBuildRequestLogMsg, buildRequest.serialise()));
    } catch (JsonMappingException e) {
      logFatalBuildRequest(buildDetails, e);
      throw new IncorrectParameterException(layersParsingErrorText);
    } catch (JsonParseException e) {
      logFatalBuildRequest(buildDetails, e);
      throw new IncorrectParameterException(layersParsingErrorText);
    } catch (IOException e) {
      logFatalBuildRequest(buildDetails, e);
      throw new IncorrectParameterException(layersParsingErrorText);
    } catch (NoSuchAlgorithmException e) {
      throw new ConfigurationException(missingAlgorithmErrorText);
    }
   
    return buildRequest;
  }
 
  /**
   * Return full URL link for a build resource.
   *
   * @param request - Request param, used to access path info.
   * @param digest - Resource identifier
   * @return Full URL for build resource
   * @throws MalformedURLException
   */
  protected String layersBuildResourcePath(HttpServletRequest request, String digest) throws ConfigurationException {
    return "/api/build/" + digest;   
  }

  /**
   * Return full URL link for a the status resource for a build.
   *
   * @param request - Request param, used to access path info.
   * @param digest - Resource identifier
   * @return Full URL for build resource
   * @throws MalformedURLException
   */
  protected String buildRequestStatusPath(HttpServletRequest request, String reference) throws ConfigurationException {
    return request.getRequestURI() + "/status/" + reference;
   
  }
 
  /**
   * Pull out the JavaScript packages referenced in this
   * build request. All packages must reference known packages in our
   * repository and must contain the Dojo package.
   *
   * @param buildRequest - User's build request
   * @return Reference packages for this build
   * @throws IncorrectParameterException - Unable to find valid dojo package reference
   */
  protected List<Map<String, String>> extractBuildPackages(Map<String, Object> buildRequest)
  throws IncorrectParameterException {   
    List<Map<String, String>> packages
      = (List<Map<String, String>>) extractMandatoryParameter(buildRequest, "packages");
   
    // Dojo package reference is mandatory for all build requests
    boolean containsDojoPackage = false;
   
    // Verify all reference packages are present in our repository
    Iterator<Map<String, String>> it = packages.iterator();
      while (it.hasNext()) {
          Map<String, String> entry = it.next();
          String name = extractMandatoryParameter(entry, "name"),
            version = extractMandatoryParameter(entry, "version");
          if (!repo.packageVersionExists(name, version)) {
            throw new IncorrectParameterException(String.format(invalidPackageErrorText, name));   
          } else if (name.equals("dojo")) {
            containsDojoPackage = true;
          }
      }
   
      // Dojo package is mandatory!
    if (!containsDojoPackage) {
      throw new IncorrectParameterException(String.format(invalidPackageErrorText, "dojo"));
    }
   
    return packages;
  }
  
  /**
   * Retrieve and verify (optional) layers parameters from build request. If parameter
   * is present, verify each dependency in every layer references a defined package.
   *
   * @param buildRequest - Request parameters
   * @param validPackages - Set of referenced packages
   * @return User's build layers
   */
  protected List<Map<String, Object>> extractBuildLayers(Map<String, Object> buildRequest,
    Set<String> validPackages) {
    // Build should always contains default dojo layer
    boolean containsBaseDojoLayer = false;
   
    List<Map<String, Object>> layers = new ArrayList<Map<String, Object>>();
       
    // For every user layer, confirm every referenced dependency
    // refers to a valid package.
    if (buildRequest.containsKey("layers")) {
      layers = (List<Map<String, Object>>) buildRequest.get("layers");
      Iterator<Map<String, Object>> layerIter = layers.iterator();
     
      while(layerIter.hasNext()) {
        // For each module layer....
        Map<String, Object> layer = layerIter.next();
        List<Map<String, String>> dependencies = (List<Map<String, String>>) layer.get("modules");
        //.. verify each one is valid.
        verifyModuleDependencies(dependencies, validPackages);
       
        // Check each layer to see if we come across dojo base
        containsBaseDojoLayer |= isBaseDojoLayer(layer);
      }
    }
   
    // If user hasn't explicitly asked for base dojo layer,
    // ensure it's present. This layer will always be generated
    // by the build system.
    if (!containsBaseDojoLayer) {           
      layers.add(defaultDojoLayer);
    }
   
    return layers;
  }
 
  /**
   * Confirm each dependency module has a recognised package.
   * Iterate through the list checking all dependencies against
   * package set. 
   *
   * @param dependencies - Module dependencies
   * @param validPackages - All valid packages
   */
  protected void verifyModuleDependencies(List<Map<String, String>> dependencies, Set<String> validPackages) {
    Iterator<Map<String, String>> depIter = dependencies.iterator();
    // ... pull out each dependency
    while (depIter.hasNext()) {
      Map<String, String> dependency = depIter.next();
      String packageId = dependency.get("package");
      // ... and check it has a valid package reference
      if (packageId == null || !validPackages.contains(packageId)) {
        throw new IncorrectParameterException(String.format(invalidModulePackageErrorText, dependency.get("name")));
      }
    }
  }
 
  /**
   * Check with custom layer matches dojo base layer.
   * Compare layer identifiers.
   *
   * @param buildLayer - User layer
   * @return Custom layer represent's dojo base
   */
  protected Boolean isBaseDojoLayer(Map<String, Object> buildLayer) {
    String defaultLayerName =  (String) defaultDojoLayer.get("name"),
      buildLayerName = (String) buildLayer.get("name");
   
    return defaultLayerName.equals(buildLayerName);
  }
 
  /**
   * Retrieve mandatory parameter from the build request. Any missing
   * parameters are fatal errors and cause exceptions to be thrown.
   * @param <T>
   *
   * @param request - Build request details
   * @param identifier - Parameter identifier
   * @throws IncorrectParameterException - Missing mandatory build parameter
   */
  protected <T> T extractMandatoryParameter(Map<String, T> request, String identifier) {
    if (!request.containsKey(identifier) || request.get(identifier) == null) {
      throw new IncorrectParameterException(String.format(missingParameterErrorText, identifier));
    }
   
    return request.get(identifier);
  }
 
  /**
   * Log details of the error caught when trying to process a user's
   * build request. Attempt to serialise original build request object.
   *
   * @param buildDetails - User's build request
   * @param e - Exception throw during processing
   */
  protected void logFatalBuildRequest(Map<String, Object> buildDetails, Exception e) {
    String serialisedBuildDetails;
    try {
      serialisedBuildDetails = JsonUtil.writeJavaToJson(buildDetails);
    } catch (JsonParseException exception) {
      serialisedBuildDetails = exception.getMessage();
    } catch (JsonMappingException exception) {
      serialisedBuildDetails = exception.getMessage();
    } catch (IOException exception) {
      serialisedBuildDetails = exception.getMessage();
    }
   
    logger.log(Level.SEVERE, String.format(fatalBuildRequestLogMsg, e.getMessage(), serialisedBuildDetails));
  }
}
TOP

Related Classes of org.dtk.resources.Build

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.