/** Receives does nothing - send will put the response in the same buffer */
  public int receive(Msg msg, MsgContext ep) throws IOException {
    Msg sentResponse = (Msg) ep.getNote(receivedNote);
    ep.setNote(receivedNote, null);

    if (sentResponse == null) {
      if (LoggerUtils.getLogger().isLoggable(Level.FINEST))
        LoggerUtils.getLogger().log(Level.FINEST, "No send() prior to receive(), no data buffer");
      // No sent() was done prior to receive.
      msg.reset();
      msg.end();
      sentResponse = msg;
    }

    sentResponse.processHeader();

    if (LoggerUtils.getLogger().isLoggable(Level.FINE)) sentResponse.dump("received response ");

    if (msg != sentResponse) {
      LoggerUtils.getLogger()
          .severe(
              "Error, in JNI mode the msg used for receive() must be identical with the one used for send()");
    }

    return 0;
  }
  /** Send the packet. XXX This will modify msg !!! We could use 2 packets, or sendAndReceive(). */
  public int send(Msg msg, MsgContext ep) throws IOException {
    ep.setNote(receivedNote, null);
    if (LoggerUtils.getLogger().isLoggable(Level.FINEST))
      LoggerUtils.getLogger().log(Level.FINEST, "ChannelJni.send: " + msg);

    int rc = super.nativeDispatch(msg, ep, JK_HANDLE_JNI_DISPATCH, 0);

    // nativeDispatch will put the response in the same buffer.
    // Next receive() will just get it from there. Very tricky to do
    // things in one thread instead of 2.
    ep.setNote(receivedNote, msg);

    return rc;
  }
  public void init() throws IOException {
    super.initNative("channel.jni:jni");

    if (apr == null) return;

    // We'll be called from C. This deals with that.
    apr.addJkHandler("channelJni", this);
    LoggerUtils.getLogger().info("JK: listening on channel.jni:jni");

    if (next == null) {
      if (nextName != null) setNext(wEnv.getHandler(nextName));
      if (next == null) next = wEnv.getHandler("dispatch");
      if (next == null) next = wEnv.getHandler("request");
      if (LoggerUtils.getLogger().isLoggable(Level.FINEST))
        LoggerUtils.getLogger()
            .log(Level.FINEST, "Setting default next " + next.getClass().getName());
    }
  }
  /**
   * Receive a packet from the C side. This is called from the C code using invocation, but only for
   * the first packet - to avoid recursivity and thread problems.
   *
   * <p>This may look strange, but seems the best solution for the problem ( the problem is that we
   * don't have 'continuation' ).
   *
   * <p>sendPacket will move the thread execution on the C side, and return when another packet is
   * available. For packets that are one way it'll return after it is processed too ( having 2
   * threads is far more expensive ).
   *
   * <p>Again, the goal is to be efficient and behave like all other Channels ( so the rest of the
   * code can be shared ). Playing with java objects on C is extremely difficult to optimize and do
   * right ( IMHO ), so we'll try to keep it simple - byte[] passing, the conversion done in java (
   * after we know the encoding and if anyone asks for it - same lazy behavior as in 3.3 ).
   */
  public int invoke(Msg msg, MsgContext ep) throws IOException {
    if (apr == null) return -1;

    long xEnv = ep.getJniEnv();
    long cEndpointP = ep.getJniContext();

    int type = ep.getType();
    if (LoggerUtils.getLogger().isLoggable(Level.FINEST))
      LoggerUtils.getLogger().log(Level.FINEST, "ChannelJni.invoke: " + ep + " " + type);

    switch (type) {
      case JkHandler.HANDLE_RECEIVE_PACKET:
        return receive(msg, ep);
      case JkHandler.HANDLE_SEND_PACKET:
        return send(msg, ep);
      case JkHandler.HANDLE_FLUSH:
        return flush(msg, ep);
    }

    // Reset receivedNote. It'll be visible only after a SEND and before a receive.
    ep.setNote(receivedNote, null);

    // Default is FORWARD - called from C
    try {
      // first, we need to get an endpoint. It should be
      // per/thread - and probably stored by the C side.
      if (LoggerUtils.getLogger().isLoggable(Level.FINEST))
        LoggerUtils.getLogger().log(Level.FINEST, "Received request " + xEnv);

      // The endpoint will store the message pt.
      msg.processHeader();

      if (LoggerUtils.getLogger().isLoggable(Level.FINE)) msg.dump("Incoming msg ");

      int status = next.invoke(msg, ep);

      if (LoggerUtils.getLogger().isLoggable(Level.FINEST))
        LoggerUtils.getLogger().log(Level.FINEST, "after processCallbacks " + status);

      return status;
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    return 0;
  }
/**
 * Simple {@link Adapter} that map the {@link Request} URI to a local file. The file is send
 * synchronously using the NIO send file mechanism (@link File#transfertTo}.
 *
 * <p>This class doesn't not decode the {@link Request} uri and just do basic security check. If you
 * need more protection, use the {@link GrizzlyAdapter} class instead or extend the {@link
 * StaticResourcesAdapter#service} and use {@link HttpRequestURIDecoder} to protect against security
 * attack.
 *
 * @author Jeanfrancois Arcand
 */
public class StaticResourcesAdapter implements Adapter {

  private static final String USE_SEND_FILE = "com.sun.grizzly.useSendFile";
  private final Queue<String> rootFolders = DataStructures.getCLQinstance(String.class);
  protected String resourcesContextPath = "";
  protected final Queue<File> fileFolders = DataStructures.getCLQinstance(File.class);
  protected final ConcurrentHashMap<String, File> cache = new ConcurrentHashMap<String, File>();
  protected Logger logger = LoggerUtils.getLogger();
  private boolean useSendFile = true;
  /** Commit the 404 response automatically. */
  protected boolean commitErrorResponse = true;

  private final ReentrantLock initializedLock = new ReentrantLock();
  private String defaultContentType = "text/html";

  public StaticResourcesAdapter() {
    this(".");
  }

  public StaticResourcesAdapter(String rootFolder) {
    addRootFolder(rootFolder);

    // Ugly workaround
    // See Issue 327
    if ((System.getProperty("os.name").equalsIgnoreCase("linux") && !linuxSendFileSupported())
        || System.getProperty("os.name").equalsIgnoreCase("HP-UX")) {
      useSendFile = false;
    }
    if (System.getProperty(USE_SEND_FILE) != null) {
      useSendFile = Boolean.valueOf(System.getProperty(USE_SEND_FILE));
      logger.info("Send-file enabled:" + useSendFile);
    }
  }

  /**
   * Based on the {@link Request} URI, try to map the file from the rootFolder, and send it
   * synchronously using send file.
   *
   * @param req the {@link Request}
   * @param res the {@link Response}
   * @throws Exception
   */
  public void service(Request req, final Response res) throws Exception {
    String uri = req.requestURI().toString();
    if (uri.contains("..") || !uri.startsWith(resourcesContextPath)) {
      res.setStatus(404);
      if (commitErrorResponse) {
        customizedErrorPage(req, res);
      }
      return;
    }

    // We map only file that take the form of name.extension
    if (uri.contains(".")) {
      uri = uri.substring(resourcesContextPath.length());
    }

    service(uri, req, res);
  }

  /**
   * Lookup a resource based on the request URI, and send it using send file.
   *
   * @param uri The request URI
   * @param req the {@link Request}
   * @param res the {@link Response}
   * @throws Exception
   */
  protected void service(String uri, Request req, final Response res) throws Exception {
    FileInputStream fis = null;
    try {
      initWebDir();

      boolean found = false;
      File resource = null;

      for (File webDir : fileFolders) {
        // local file
        resource = cache.get(uri);
        if (resource == null) {
          resource = new File(webDir, uri);
          if (resource.exists() && resource.isDirectory()) {
            final File f = new File(resource, "/index.html");
            if (f.exists()) {
              resource = f;
              found = true;
              break;
            }
          }
        }

        if (resource.isDirectory() || !resource.exists()) {
          found = false;
        } else {
          found = true;
          break;
        }
      }

      cache.put(uri, resource);
      if (!found) {
        if (logger.isLoggable(Level.FINE)) {
          logger.log(Level.FINE, "File not found  " + resource);
        }
        res.setStatus(404);
        if (commitErrorResponse) {
          customizedErrorPage(req, res);
        }
        return;
      }

      res.setStatus(200);
      String substr;
      int dot = uri.lastIndexOf(".");
      if (dot < 0) {
        substr = resource.toString();
        dot = substr.lastIndexOf(".");
      } else {
        substr = uri;
      }
      if (dot > 0) {
        String ext = substr.substring(dot + 1);
        String ct = MimeType.get(ext, defaultContentType);
        if (ct != null) {
          res.setContentType(ct);
        }
      } else {
        res.setContentType(defaultContentType);
      }

      long length = resource.length();
      res.setContentLengthLong(length);

      // Send the header, and flush the bytes as we will now move to use
      // send file.
      res.sendHeaders();

      if (req.method().toString().equalsIgnoreCase("HEAD")) {
        return;
      }

      fis = new FileInputStream(resource);
      OutputBuffer outputBuffer = res.getOutputBuffer();

      if (useSendFile
          && (outputBuffer instanceof FileOutputBuffer)
          && ((FileOutputBuffer) outputBuffer).isSupportFileSend()) {
        res.flush();

        long nWrite = 0;
        while (nWrite < length) {
          nWrite +=
              ((FileOutputBuffer) outputBuffer).sendFile(fis.getChannel(), nWrite, length - nWrite);
        }
      } else {
        byte b[] = new byte[8192];
        ByteChunk chunk = new ByteChunk();
        int rd;
        while ((rd = fis.read(b)) > 0) {
          chunk.setBytes(b, 0, rd);
          res.doWrite(chunk);
        }
      }
    } finally {
      if (fis != null) {
        try {
          fis.close();
        } catch (IOException ignored) {
        }
      }
    }
  }

  /**
   * Customize the error pahe
   *
   * @param req The {@link Request} object
   * @param res The {@link Response} object
   * @throws Exception
   */
  protected void customizedErrorPage(Request req, Response res) throws Exception {

    /** With Grizzly, we just return a 404 with a simple error message. */
    res.setMessage("Not Found");
    res.setStatus(404);
    ByteBuffer bb = HtmlHelper.getErrorPage("Not Found", "HTTP/1.1 404 Not Found\r\n", "Grizzly");
    res.setContentLength(bb.limit());
    res.setContentType("text/html");
    res.flushHeaders();
    if (res.getChannel() != null) {
      res.getChannel().write(bb);
      req.setNote(14, "SkipAfterService");
    } else {
      byte b[] = new byte[bb.limit()];
      bb.get(b);
      ByteChunk chunk = new ByteChunk();
      chunk.setBytes(b, 0, b.length);
      res.doWrite(chunk);
    }
  }

  /**
   * Finish the {@link Response} and recycle the {@link Request} and the {@link Response}. If the
   * {@link StaticResourcesAdapter#commitErrorResponse} is set to false, this method does nothing.
   *
   * @param req {@link Request}
   * @param res {@link Response}
   * @throws Exception
   */
  public void afterService(Request req, Response res) throws Exception {
    if (req.getNote(14) != null) {
      req.setNote(14, null);
      return;
    }

    if (res.getStatus() == 404 && !commitErrorResponse) {
      return;
    }

    try {
      req.action(ActionCode.ACTION_POST_REQUEST, null);
    } catch (Throwable t) {
      logger.log(Level.WARNING, "afterService unexpected exception: ", t);
    }

    res.finish();
  }

  /**
   * Return the directory from where files will be serviced.
   *
   * @return the directory from where file will be serviced.
   * @deprecated - use {@link #getRootFolders}
   */
  public String getRootFolder() {
    return rootFolders.peek();
  }

  /**
   * Set the directory from where files will be serviced.
   *
   * <p>NOTE: For backward compatibility, invoking that method will clear all previous values added
   * using {@link #addRootFolder}.
   *
   * @param rootFolder the directory from where files will be serviced.
   * @deprecated - use {@link #addRootFolder}
   */
  public void setRootFolder(String rootFolder) {
    rootFolders.clear();
    addRootFolder(rootFolder);
  }

  /**
   * Return the list of folders the adapter can serve file from.
   *
   * @return a {@link Queue} of the folders this Adapter can serve file from.
   */
  public Queue<String> getRootFolders() {
    return rootFolders;
  }

  /**
   * Add a folder to the list of folders this Adapter can serve file from.
   *
   * @param rootFolder
   * @return
   */
  public boolean addRootFolder(String rootFolder) {
    return rootFolders.offer(rootFolder);
  }

  /** Initialize. */
  protected void initWebDir() throws IOException {
    try {
      initializedLock.lock();
      if (fileFolders.isEmpty()) {
        for (String s : rootFolders) {
          File webDir = new File(s);
          fileFolders.offer(webDir);
        }
        rootFolders.clear();

        for (File f : fileFolders) {
          rootFolders.add(f.getCanonicalPath());
        }
      }
    } finally {
      initializedLock.unlock();
    }
  }

  @SuppressWarnings("UnusedDeclaration")
  public void setLogger(Logger logger) {
    this.logger = logger;
  }

  /**
   * @return <code>true</code> if {@link java.nio.channels.FileChannel#transferTo(long, long,
   *     java.nio.channels.WritableByteChannel)} to send a static resources.
   */
  @SuppressWarnings("UnusedDeclaration")
  public boolean isUseSendFile() {
    return useSendFile;
  }

  /**
   * <code>true</code> if {@link java.nio.channels.FileChannel#transferTo(long, long,
   * java.nio.channels.WritableByteChannel)} to send a static resources, false if the File needs to
   * be loaded in memory and flushed using {@link ByteBuffer}.
   *
   * @param useSendFile True if {@link java.nio.channels.FileChannel#transferTo(long, long,
   *     java.nio.channels.WritableByteChannel)} to send a static resources, false if the File needs
   *     to be loaded in memory and flushed using {@link ByteBuffer}
   */
  public void setUseSendFile(boolean useSendFile) {
    this.useSendFile = useSendFile;
  }

  /**
   * Return the context path used for servicing resources. By default, "" is used so request taking
   * the form of http://host:port/index.html are serviced directly. If set, the resource will be
   * available under http://host:port/context-path/index.html
   *
   * @return the context path.
   */
  @SuppressWarnings("UnusedDeclaration")
  public String getResourcesContextPath() {
    return resourcesContextPath;
  }

  /**
   * Set the context path used for servicing resource. By default, "" is used so request taking the
   * form of http://host:port/index.html are serviced directly. If set, the resource will be
   * available under http://host:port/context-path/index.html
   *
   * @param resourcesContextPath the context path
   */
  public void setResourcesContextPath(String resourcesContextPath) {
    this.resourcesContextPath = resourcesContextPath;
  }

  /**
   * If the content-type of the request cannot be determined, used the default value. Current
   * default is text/html
   *
   * @return the defaultContentType
   */
  @SuppressWarnings("UnusedDeclaration")
  public String getDefaultContentType() {
    return defaultContentType;
  }

  /**
   * Set the default content-type if we can't determine it. Default was text/html
   *
   * @param defaultContentType the defaultContentType to set
   */
  public void setDefaultContentType(String defaultContentType) {
    this.defaultContentType = defaultContentType;
  }

  private static boolean linuxSendFileSupported(final String jdkVersion) {
    if (jdkVersion.startsWith("1.6")) {
      int idx = jdkVersion.indexOf('_');
      if (idx == -1) {
        return false;
      }
      StringBuilder sb = new StringBuilder(3);
      final String substr = jdkVersion.substring(idx + 1);
      int len = Math.min(substr.length(), 3);
      for (int i = 0; i < len; i++) {
        final char c = substr.charAt(i);
        if (Character.isDigit(c)) {
          sb.append(c);
          continue;
        }
        break;
      }
      if (sb.length() == 0) {
        return false;
      }
      final int patchRev = Integer.parseInt(sb.toString());
      return (patchRev >= 18);
    } else {
      return jdkVersion.startsWith("1.7") || jdkVersion.startsWith("1.8");
    }
  }

  private static boolean linuxSendFileSupported() {
    return linuxSendFileSupported(System.getProperty("java.version"));
  }
}