LauncherDir.java

/*
 * Copyright 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.codeserver;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.codeserver.CompileDir.PolicyFile;
import com.google.gwt.dev.resource.impl.ResourceOracleImpl;
import com.google.gwt.thirdparty.guava.common.base.Charsets;
import com.google.gwt.thirdparty.guava.common.base.Preconditions;
import com.google.gwt.thirdparty.guava.common.io.Files;
import com.google.gwt.thirdparty.guava.common.io.Resources;

import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 * An output directory where other servers pick up files from Super Dev Mode.
 * This often points to the -war directory for a DevMode server,
 * but it may point to a subdirectory.
 *
 * <p>Underneath this directory, the code server will write one subdirectory for each module.
 * A nocache.js file, public resource files, and GWT-RPC policy files will be written there.
 * This is usually enough to launch Super Dev Mode without running the
 * production GWT compiler first.
 */
class LauncherDir {
  private final File launcherDir;
  private final Options options;

  /**
   * @see #maybeCreate
   */
  private LauncherDir(Options options) {
    this.launcherDir = Preconditions.checkNotNull(options.getLauncherDir());
    this.options = options;
  }

  /**
   * Updates files after a successful compile (or on startup).
   * @param module the module that was compiled
   * @param compileDir the compiler's output directory
   */
  void update(ModuleDef module, CompileDir compileDir, TreeLogger logger)
      throws UnableToCompleteException {
    File moduleDir = new File(launcherDir + "/" + module.getName());
    if (!moduleDir.isDirectory()) {
      if (!moduleDir.mkdirs()) {
        logger.log(Type.ERROR, "Can't create launcher dir for module: " + moduleDir);
        throw new UnableToCompleteException();
      }
    }

    try {
      String stub = generateStubNocacheJs(module.getName(), options);

      final File noCacheJs = new File(moduleDir, module.getName() + ".nocache.js");
      Files.write(stub, noCacheJs, Charsets.UTF_8);

      // Remove gz file so it doesn't get used instead.
      // (We may be writing to an existing war directory.)
      final File noCacheJsGz = new File(noCacheJs.getPath() + ".gz");
      if (noCacheJsGz.exists()) {
        if (!noCacheJsGz.delete()) {
          logger.log(Type.ERROR, "cannot delete file: " + noCacheJsGz);
          throw new UnableToCompleteException();
        }
      }

      writePublicResources(moduleDir, module, logger);

      // Copy the GWT-RPC serialization policies so that the subclass of RemoteServiceServlet
      // can pick it up. (It expects to find policy files in the module directory.)
      // See RemoteServiceServlet.loadSerializationPolicy.
      // (An alternate approach is to set the gwt.codeserver.port environment variable
      // so that the other server downloads policies over HTTP.)
      for (PolicyFile policyFile : compileDir.readRpcPolicyManifest(module.getName())) {
        String filename = policyFile.getName();
        File src = new File(compileDir.getWarDir(), module.getName() + "/" + filename);
        File dest = new File(moduleDir, filename);
        Files.copy(src, dest);
      }

    } catch (IOException e) {
      logger.log(Type.ERROR, "Can't update launcher dir", e);
      throw new UnableToCompleteException();
    }
  }

  /**
   * Returns a new LauncherDir or null if not enabled.
   */
  static LauncherDir maybeCreate(Options options) {
    if (options.getLauncherDir() == null) {
      return null;
    }
    return new LauncherDir(options);
  }

  // TODO: figure out a better home for these static methods.

  /**
   * Returns the contents of a nocache.js file that will compile and then run a GWT application
   * using Super Dev Mode.
   */
  static String generateStubNocacheJs(String outputModuleName, Options options) throws IOException {
    URL url = Resources.getResource(Recompiler.class, "stub.nocache.js");
    final String template = Resources.toString(url, Charsets.UTF_8);
    return template
        .replace("__MODULE_NAME__", outputModuleName)
        .replace("__SUPERDEV_PORT__", String.valueOf(options.getPort()));
  }

  static void writePublicResources(File moduleOutputDir, ModuleDef module,
      TreeLogger compileLogger) throws UnableToCompleteException, IOException {
    // Copy the public resources to the output.
    ResourceOracleImpl publicResources = module.getPublicResourceOracle();
    for (String pathName : publicResources.getPathNames()) {
      File file = new File(moduleOutputDir, pathName);
      File parent = file.getParentFile();
      if (!parent.isDirectory() && !parent.mkdirs()) {
        compileLogger.log(Type.ERROR, "cannot create directory: " + parent);
        throw new UnableToCompleteException();
      }
      Files.asByteSink(file).writeFrom(publicResources.getResourceAsStream(pathName));
    }
  }
}