WEB-15327 Can you support server side includes in preview server
authorVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Thu, 19 Feb 2015 14:07:11 +0000 (15:07 +0100)
committerVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Thu, 19 Feb 2015 14:11:24 +0000 (15:11 +0100)
15 files changed:
platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java
platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java
platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionParseTree.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionTokenizer.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiCommand.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiConditional.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiExternalResolver.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiFsize.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessingState.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessor.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiStopProcessingException.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/Strftime.java [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/io/FileResponses.java
platform/platform-impl/src/io/netty/buffer/ByteBufUtf8Writer.java

index b4e2e4fc6048e75e85b4120b707ec01cd856a6d0..220279e9850925686cdbf3f5d0438037470d2ed2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2000-2014 JetBrains s.r.o.
+ * Copyright 2000-2015 JetBrains s.r.o.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,11 +21,15 @@ import com.intellij.openapi.project.ProjectManager;
 import com.intellij.openapi.util.SystemInfoRt;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.util.text.StringUtilRt;
 import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.PathUtilRt;
 import com.intellij.util.UriUtil;
 import com.intellij.util.io.URLUtil;
 import com.intellij.util.net.NetUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtf8Writer;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
@@ -34,6 +38,8 @@ import io.netty.handler.codec.http.*;
 import io.netty.handler.stream.ChunkedStream;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver;
+import org.jetbrains.builtInWebServer.ssi.SsiProcessor;
 import org.jetbrains.ide.HttpRequestHandler;
 import org.jetbrains.io.FileResponses;
 import org.jetbrains.io.Responses;
@@ -43,6 +49,8 @@ import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
+import static org.jetbrains.io.Responses.addKeepAliveIfNeed;
+
 public final class BuiltInWebServer extends HttpRequestHandler {
   static final Logger LOG = Logger.getInstance(BuiltInWebServer.class);
 
@@ -59,6 +67,7 @@ public final class BuiltInWebServer extends HttpRequestHandler {
       for (VirtualFile child : children) {
         if (!child.isDirectory()) {
           String name = child.getName();
+          //noinspection IfStatementWithIdenticalBranches
           if (name.equals(preferredName)) {
             return child;
           }
@@ -81,7 +90,7 @@ public final class BuiltInWebServer extends HttpRequestHandler {
 
   @Override
   public boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) {
-    String host = HttpHeaders.getHost(request);
+    String host = request.headers().get(HttpHeaderNames.HOST);
     if (StringUtil.isEmpty(host)) {
       return false;
     }
@@ -178,13 +187,23 @@ public final class BuiltInWebServer extends HttpRequestHandler {
   }
 
   static final class StaticFileHandler extends WebServerFileHandler {
+    private SsiProcessor ssiProcessor;
+
     @Override
     public boolean process(@NotNull VirtualFile file,
                            @NotNull CharSequence canonicalRequestPath,
                            @NotNull Project project,
                            @NotNull FullHttpRequest request,
-                           @NotNull Channel channel) throws IOException {
+                           @NotNull Channel channel,
+                           boolean isCustomHost) throws IOException {
       if (file.isInLocalFileSystem()) {
+        CharSequence nameSequence = file.getNameSequence();
+        //noinspection SpellCheckingInspection
+        if (StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtml") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".stm") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtm")) {
+          processSsi(file, canonicalRequestPath, project, request, channel, isCustomHost);
+          return true;
+        }
+
         File ioFile = VfsUtilCore.virtualToIoFile(file);
         if (hasAccess(ioFile)) {
           FileResponses.sendFile(request, channel, ioFile);
@@ -199,9 +218,9 @@ public final class BuiltInWebServer extends HttpRequestHandler {
           return true;
         }
 
-        boolean keepAlive = Responses.addKeepAliveIfNeed(response, request);
+        boolean keepAlive = addKeepAliveIfNeed(response, request);
         if (request.method() != HttpMethod.HEAD) {
-          HttpHeaders.setContentLength(response, file.getLength());
+          HttpHeaderUtil.setContentLength(response, file.getLength());
         }
 
         channel.write(response);
@@ -218,6 +237,56 @@ public final class BuiltInWebServer extends HttpRequestHandler {
       return true;
     }
 
+    private void processSsi(@NotNull VirtualFile file,
+                            @NotNull CharSequence canonicalRequestPath,
+                            @NotNull Project project,
+                            @NotNull FullHttpRequest request, @NotNull Channel channel, boolean isCustomHost) throws IOException {
+      String path = PathUtilRt.getParentPath(canonicalRequestPath.toString());
+      if (!isCustomHost) {
+        // remove project name - SSI resolves files only inside current project
+        path = path.substring(path.indexOf('/', 1) + 1);
+      }
+
+      if (ssiProcessor == null) {
+        ssiProcessor = new SsiProcessor(false);
+      }
+
+      ByteBuf buffer = channel.alloc().ioBuffer();
+      boolean keepAlive;
+      boolean releaseBuffer = true;
+      try {
+        long lastModified = ssiProcessor.process(new SsiExternalResolver(project, request, path, file.getParent()),
+                                                 VfsUtilCore.loadText(file), file.getTimeStamp(), new ByteBufUtf8Writer(buffer));
+
+        HttpResponse response = FileResponses.prepareSend(request, channel, lastModified, file.getPath());
+        if (response == null) {
+          return;
+        }
+
+        keepAlive = addKeepAliveIfNeed(response, request);
+        if (request.method() != HttpMethod.HEAD) {
+          HttpHeaderUtil.setContentLength(response, buffer.readableBytes());
+        }
+
+        channel.write(response);
+
+        if (request.method() != HttpMethod.HEAD) {
+          releaseBuffer = false;
+          channel.write(buffer);
+        }
+      }
+      finally {
+        if (releaseBuffer) {
+          buffer.release();
+        }
+      }
+
+      ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
+      if (!keepAlive) {
+        future.addListener(ChannelFutureListener.CLOSE);
+      }
+    }
+
     private static boolean hasAccess(File result) {
       // deny access to .htaccess files
       return !result.isDirectory() && result.canRead() && !(result.isHidden() || result.getName().startsWith(".ht"));
index 9cef834f005241041ab3b27a9fd1196696c1e13b..61094173ba3369863c93236d8619c28e0004126d 100644 (file)
@@ -90,7 +90,7 @@ final class DefaultWebServerPathHandler extends WebServerPathHandler {
 
     for (WebServerFileHandler fileHandler : WebServerFileHandler.EP_NAME.getExtensions()) {
       try {
-        if (fileHandler.process(result, canonicalRequestPath, project, request, channel)) {
+        if (fileHandler.process(result, canonicalRequestPath, project, request, channel, isCustomHost)) {
           return true;
         }
       }
index 9035af6c52cc826e74aa79202e36b4a857125031..3d846ca74d44e85bdd7558715adcdec79e2b67a7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2000-2014 JetBrains s.r.o.
+ * Copyright 2000-2015 JetBrains s.r.o.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -31,5 +31,6 @@ public abstract class WebServerFileHandler {
                                   @NotNull CharSequence canonicalRequestPath,
                                   @NotNull Project project,
                                   @NotNull FullHttpRequest request,
-                                  @NotNull Channel channel) throws IOException;
+                                  @NotNull Channel channel,
+                                  boolean isCustomHost) throws IOException;
 }
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionParseTree.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionParseTree.java
new file mode 100644 (file)
index 0000000..e11ad6e
--- /dev/null
@@ -0,0 +1,448 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jetbrains.builtInWebServer.ssi;
+
+import java.text.ParseException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Represents a parsed expression.
+ *
+ * @author Paul Speed
+ */
+final class ExpressionParseTree {
+  /**
+   * Contains the current set of completed nodes. This is a workspace for the
+   * parser.
+   */
+  private final LinkedList<Node> nodeStack = new LinkedList<Node>();
+  /**
+   * Contains operator nodes that don't yet have values. This is a workspace
+   * for the parser.
+   */
+  private final LinkedList<OppNode> oppStack = new LinkedList<OppNode>();
+  /**
+   * The root node after the expression has been parsed.
+   */
+  private Node root;
+  /**
+   * The SSIMediator to use when evaluating the expressions.
+   */
+  private final SsiProcessingState mySsiProcessingState;
+
+  /**
+   * Creates a new parse tree for the specified expression.
+   */
+  public ExpressionParseTree(String expr, SsiProcessingState ssiProcessingState)
+    throws ParseException {
+    this.mySsiProcessingState = ssiProcessingState;
+    parseExpression(expr);
+  }
+
+
+  /**
+   * Evaluates the tree and returns true or false. The specified SSIMediator
+   * is used to resolve variable references.
+   */
+  public boolean evaluateTree() {
+    return root.evaluate();
+  }
+
+
+  /**
+   * Pushes a new operator onto the opp stack, resolving existing opps as
+   * needed.
+   */
+  private void pushOpp(OppNode node) {
+    // If node is null then it's just a group marker
+    if (node == null) {
+      oppStack.add(0, node);
+      return;
+    }
+    while (true) {
+      if (oppStack.size() == 0) break;
+      OppNode top = oppStack.get(0);
+      // If the top is a spacer then don't pop
+      // anything
+      //noinspection ConstantConditions
+      if (top == null) break;
+      // If the top node has a lower precedence then
+      // let it stay
+      if (top.getPrecedence() < node.getPrecedence()) break;
+      // Remove the top node
+      oppStack.remove(0);
+      // Let it fill its branches
+      top.popValues(nodeStack);
+      // Stick it on the resolved node stack
+      nodeStack.add(0, top);
+    }
+    // Add the new node to the opp stack
+    oppStack.add(0, node);
+  }
+
+
+  /**
+   * Resolves all pending opp nodes on the stack until the next group marker
+   * is reached.
+   */
+  private void resolveGroup() {
+    OppNode top;
+    while ((top = oppStack.remove(0)) != null) {
+      // Let it fill its branches
+      top.popValues(nodeStack);
+      // Stick it on the resolved node stack
+      nodeStack.add(0, top);
+    }
+  }
+
+
+  /**
+   * Parses the specified expression into a tree of parse nodes.
+   */
+  private void parseExpression(String expr) throws ParseException {
+    StringNode currStringNode = null;
+    // We cheat a little and start an artificial
+    // group right away. It makes finishing easier.
+    pushOpp(null);
+    ExpressionTokenizer et = new ExpressionTokenizer(expr);
+    while (et.hasMoreTokens()) {
+      int token = et.nextToken();
+      if (token != ExpressionTokenizer.TOKEN_STRING) {
+        currStringNode = null;
+      }
+      switch (token) {
+        case ExpressionTokenizer.TOKEN_STRING:
+          if (currStringNode == null) {
+            currStringNode = new StringNode(et.getTokenValue());
+            nodeStack.add(0, currStringNode);
+          }
+          else {
+            // Add to the existing
+            currStringNode.value.append(" ");
+            currStringNode.value.append(et.getTokenValue());
+          }
+          break;
+        case ExpressionTokenizer.TOKEN_AND:
+          pushOpp(new AndNode());
+          break;
+        case ExpressionTokenizer.TOKEN_OR:
+          pushOpp(new OrNode());
+          break;
+        case ExpressionTokenizer.TOKEN_NOT:
+          pushOpp(new NotNode());
+          break;
+        case ExpressionTokenizer.TOKEN_EQ:
+          pushOpp(new EqualNode());
+          break;
+        case ExpressionTokenizer.TOKEN_NOT_EQ:
+          pushOpp(new NotNode());
+          // Sneak the regular node in. The NOT will
+          // be resolved when the next opp comes along.
+          oppStack.add(0, new EqualNode());
+          break;
+        case ExpressionTokenizer.TOKEN_RBRACE:
+          // Closeout the current group
+          resolveGroup();
+          break;
+        case ExpressionTokenizer.TOKEN_LBRACE:
+          // Push a group marker
+          pushOpp(null);
+          break;
+        case ExpressionTokenizer.TOKEN_GE:
+          pushOpp(new NotNode());
+          // Similar strategy to NOT_EQ above, except this
+          // is NOT less than
+          oppStack.add(0, new LessThanNode());
+          break;
+        case ExpressionTokenizer.TOKEN_LE:
+          pushOpp(new NotNode());
+          // Similar strategy to NOT_EQ above, except this
+          // is NOT greater than
+          oppStack.add(0, new GreaterThanNode());
+          break;
+        case ExpressionTokenizer.TOKEN_GT:
+          pushOpp(new GreaterThanNode());
+          break;
+        case ExpressionTokenizer.TOKEN_LT:
+          pushOpp(new LessThanNode());
+          break;
+        case ExpressionTokenizer.TOKEN_END:
+          break;
+      }
+    }
+    // Finish off the rest of the uopps
+    resolveGroup();
+    if (nodeStack.size() == 0) {
+      throw new ParseException("No nodes created.", et.getIndex());
+    }
+    if (nodeStack.size() > 1) {
+      throw new ParseException("Extra nodes created.", et.getIndex());
+    }
+    if (oppStack.size() != 0) {
+      throw new ParseException("Unused opp nodes exist.", et.getIndex());
+    }
+    root = nodeStack.get(0);
+  }
+
+  /**
+   * A node in the expression parse tree.
+   */
+  private abstract static class Node {
+    /**
+     * Return true if the node evaluates to true.
+     */
+    public abstract boolean evaluate();
+  }
+
+  /**
+   * A node the represents a String value
+   */
+  private class StringNode extends Node {
+    StringBuilder value;
+    String resolved = null;
+
+
+    public StringNode(String value) {
+      this.value = new StringBuilder(value);
+    }
+
+
+    /**
+     * Resolves any variable references and returns the value string.
+     */
+    public String getValue() {
+      if (resolved == null) {
+        resolved = mySsiProcessingState.substituteVariables(value.toString());
+      }
+      return resolved;
+    }
+
+
+    /**
+     * Returns true if the string is not empty.
+     */
+    @Override
+    public boolean evaluate() {
+      return !(getValue().length() == 0);
+    }
+
+
+    @Override
+    public String toString() {
+      return value.toString();
+    }
+  }
+
+  private static final int PRECEDENCE_NOT = 5;
+  private static final int PRECEDENCE_COMPARE = 4;
+  private static final int PRECEDENCE_LOGICAL = 1;
+
+  /**
+   * A node implementation that represents an operation.
+   */
+  private abstract static class OppNode extends Node {
+    /**
+     * The left branch.
+     */
+    Node left;
+    /**
+     * The right branch.
+     */
+    Node right;
+
+
+    /**
+     * Returns a preference level suitable for comparison to other OppNode
+     * preference levels.
+     */
+    public abstract int getPrecedence();
+
+
+    /**
+     * Lets the node pop its own branch nodes off the front of the
+     * specified list. The default pulls two.
+     */
+    public void popValues(List<Node> values) {
+      right = values.remove(0);
+      left = values.remove(0);
+    }
+  }
+
+  private static final class NotNode extends OppNode {
+    @Override
+    public boolean evaluate() {
+      return !left.evaluate();
+    }
+
+
+    @Override
+    public int getPrecedence() {
+      return PRECEDENCE_NOT;
+    }
+
+
+    /**
+     * Overridden to pop only one value.
+     */
+    @Override
+    public void popValues(List<Node> values) {
+      left = values.remove(0);
+    }
+
+
+    @Override
+    public String toString() {
+      return left + " NOT";
+    }
+  }
+
+  private static final class AndNode extends OppNode {
+    @Override
+    public boolean evaluate() {
+      if (!left.evaluate()) // Short circuit
+      {
+        return false;
+      }
+      return right.evaluate();
+    }
+
+
+    @Override
+    public int getPrecedence() {
+      return PRECEDENCE_LOGICAL;
+    }
+
+
+    @Override
+    public String toString() {
+      return left + " " + right + " AND";
+    }
+  }
+
+  private static final class OrNode extends OppNode {
+    @Override
+    public boolean evaluate() {
+      if (left.evaluate()) // Short circuit
+      {
+        return true;
+      }
+      return right.evaluate();
+    }
+
+
+    @Override
+    public int getPrecedence() {
+      return PRECEDENCE_LOGICAL;
+    }
+
+
+    @Override
+    public String toString() {
+      return left + " " + right + " OR";
+    }
+  }
+
+  private abstract static class CompareNode extends OppNode {
+    protected int compareBranches() {
+      String val1 = ((StringNode)left).getValue();
+      String val2 = ((StringNode)right).getValue();
+
+      int val2Len = val2.length();
+      if (val2Len > 1 && val2.charAt(0) == '/' &&
+          val2.charAt(val2Len - 1) == '/') {
+        // Treat as a regular expression
+        String expr = val2.substring(1, val2Len - 1);
+        try {
+          Pattern pattern = Pattern.compile(expr);
+          // Regular expressions will only ever be used with EqualNode
+          // so return zero for equal and non-zero for not equal
+          if (pattern.matcher(val1).find()) {
+            return 0;
+          }
+          else {
+            return -1;
+          }
+        }
+        catch (PatternSyntaxException e) {
+          SsiProcessor.LOG.warn("Invalid expression: " + expr, e);
+          return 0;
+        }
+      }
+      return val1.compareTo(val2);
+    }
+  }
+
+  private static final class EqualNode extends CompareNode {
+    @Override
+    public boolean evaluate() {
+      return (compareBranches() == 0);
+    }
+
+
+    @Override
+    public int getPrecedence() {
+      return PRECEDENCE_COMPARE;
+    }
+
+
+    @Override
+    public String toString() {
+      return left + " " + right + " EQ";
+    }
+  }
+
+  private static final class GreaterThanNode extends CompareNode {
+    @Override
+    public boolean evaluate() {
+      return (compareBranches() > 0);
+    }
+
+
+    @Override
+    public int getPrecedence() {
+      return PRECEDENCE_COMPARE;
+    }
+
+
+    @Override
+    public String toString() {
+      return left + " " + right + " GT";
+    }
+  }
+
+  private static final class LessThanNode extends CompareNode {
+    @Override
+    public boolean evaluate() {
+      return (compareBranches() < 0);
+    }
+
+
+    @Override
+    public int getPrecedence() {
+      return PRECEDENCE_COMPARE;
+    }
+
+
+    @Override
+    public String toString() {
+      return left + " " + right + " LT";
+    }
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionTokenizer.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/ExpressionTokenizer.java
new file mode 100644 (file)
index 0000000..482cacf
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jetbrains.builtInWebServer.ssi;
+
+/**
+ * Parses an expression string to return the individual tokens. This is
+ * patterned similar to the StreamTokenizer in the JDK but customized for SSI
+ * conditional expression parsing.
+ *
+ * @author Paul Speed
+ */
+public class ExpressionTokenizer {
+  public static final int TOKEN_STRING = 0;
+  public static final int TOKEN_AND = 1;
+  public static final int TOKEN_OR = 2;
+  public static final int TOKEN_NOT = 3;
+  public static final int TOKEN_EQ = 4;
+  public static final int TOKEN_NOT_EQ = 5;
+  public static final int TOKEN_RBRACE = 6;
+  public static final int TOKEN_LBRACE = 7;
+  public static final int TOKEN_GE = 8;
+  public static final int TOKEN_LE = 9;
+  public static final int TOKEN_GT = 10;
+  public static final int TOKEN_LT = 11;
+  public static final int TOKEN_END = 12;
+  private final char[] expr;
+  private String tokenVal = null;
+  private int index;
+  private final int length;
+
+  /**
+   * Creates a new parser for the specified expression.
+   */
+  public ExpressionTokenizer(String expr) {
+    this.expr = expr.trim().toCharArray();
+    this.length = this.expr.length;
+  }
+
+  /**
+   * Returns true if there are more tokens.
+   */
+  public boolean hasMoreTokens() {
+    return index < length;
+  }
+
+  /**
+   * Returns the current index for error reporting purposes.
+   */
+  public int getIndex() {
+    return index;
+  }
+
+  protected boolean isMetaChar(char c) {
+    return Character.isWhitespace(c) || c == '(' || c == ')' || c == '!'
+           || c == '<' || c == '>' || c == '|' || c == '&' || c == '=';
+  }
+
+
+  /**
+   * Returns the next token type and initializes any state variables
+   * accordingly.
+   */
+  public int nextToken() {
+    // Skip any leading white space
+    while (index < length && Character.isWhitespace(expr[index])) {
+      index++;
+    }
+    // Clear the current token val
+    tokenVal = null;
+    if (index == length) return TOKEN_END; // End of string
+    int start = index;
+    char currentChar = expr[index];
+    char nextChar = (char)0;
+    index++;
+    if (index < length) nextChar = expr[index];
+    // Check for a known token start
+    switch (currentChar) {
+      case '(':
+        return TOKEN_LBRACE;
+      case ')':
+        return TOKEN_RBRACE;
+      case '=':
+        return TOKEN_EQ;
+      case '!':
+        if (nextChar == '=') {
+          index++;
+          return TOKEN_NOT_EQ;
+        }
+        return TOKEN_NOT;
+      case '|':
+        if (nextChar == '|') {
+          index++;
+          return TOKEN_OR;
+        }
+        break;
+      case '&':
+        if (nextChar == '&') {
+          index++;
+          return TOKEN_AND;
+        }
+        break;
+      case '>':
+        if (nextChar == '=') {
+          index++;
+          return TOKEN_GE; // Greater than or equal
+        }
+        return TOKEN_GT; // Greater than
+      case '<':
+        if (nextChar == '=') {
+          index++;
+          return TOKEN_LE; // Less than or equal
+        }
+        return TOKEN_LT; // Less than
+      default:
+        // Otherwise it's a string
+        break;
+    }
+    int end = index;
+    if (currentChar == '"' || currentChar == '\'') {
+      // It's a quoted string and the end is the next unescaped quote
+      char endChar = currentChar;
+      boolean escaped = false;
+      start++;
+      for (; index < length; index++) {
+        if (expr[index] == '\\' && !escaped) {
+          escaped = true;
+          continue;
+        }
+        if (expr[index] == endChar && !escaped) break;
+        escaped = false;
+      }
+      end = index;
+      index++; // Skip the end quote
+    }
+    else if (currentChar == '/') {
+      // It's a regular expression and the end is the next unescaped /
+      char endChar = currentChar;
+      boolean escaped = false;
+      for (; index < length; index++) {
+        if (expr[index] == '\\' && !escaped) {
+          escaped = true;
+          continue;
+        }
+        if (expr[index] == endChar && !escaped) break;
+        escaped = false;
+      }
+      end = ++index;
+    }
+    else {
+      // End is the next whitespace character
+      for (; index < length; index++) {
+        if (isMetaChar(expr[index])) break;
+      }
+      end = index;
+    }
+    // Extract the string from the array
+    this.tokenVal = new String(expr, start, end - start);
+    return TOKEN_STRING;
+  }
+
+  /**
+   * Returns the String value of the token if it was type TOKEN_STRING.
+   * Otherwise null is returned.
+   */
+  public String getTokenValue() {
+    return tokenVal;
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiCommand.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiCommand.java
new file mode 100644 (file)
index 0000000..448bbea
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.builtInWebServer.ssi;
+
+import io.netty.buffer.ByteBufUtf8Writer;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public interface SsiCommand {
+  long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer);
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiConditional.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiConditional.java
new file mode 100644 (file)
index 0000000..52f727c
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jetbrains.builtInWebServer.ssi;
+
+import io.netty.buffer.ByteBufUtf8Writer;
+import org.jetbrains.annotations.NotNull;
+
+import java.text.ParseException;
+import java.util.List;
+
+/**
+ * SSI command that handles all conditional directives.
+ *
+ * @author Paul Speed
+ * @author David Becker
+ */
+public class SsiConditional implements SsiCommand {
+  @SuppressWarnings("SpellCheckingInspection")
+  @Override
+  public long process(@NotNull SsiProcessingState ssiProcessingState, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+    // Assume anything using conditionals was modified by it
+    long lastModified = System.currentTimeMillis();
+    // Retrieve the current state information
+    SsiProcessingState.SsiConditionalState state = ssiProcessingState.conditionalState;
+    if ("if".equalsIgnoreCase(commandName)) {
+      // Do nothing if we are nested in a false branch
+      // except count it
+      if (state.processConditionalCommandsOnly) {
+        state.nestingCount++;
+        return lastModified;
+      }
+      state.nestingCount = 0;
+      // Evaluate the expression
+      if (evaluateArguments(paramNames, paramValues, ssiProcessingState)) {
+        // No more branches can be taken for this if block
+        state.branchTaken = true;
+      }
+      else {
+        // Do not process this branch
+        state.processConditionalCommandsOnly = true;
+        state.branchTaken = false;
+      }
+    }
+    else if ("elif".equalsIgnoreCase(commandName)) {
+      // No need to even execute if we are nested in
+      // a false branch
+      if (state.nestingCount > 0) return lastModified;
+      // If a branch was already taken in this if block
+      // then disable output and return
+      if (state.branchTaken) {
+        state.processConditionalCommandsOnly = true;
+        return lastModified;
+      }
+      // Evaluate the expression
+      if (evaluateArguments(paramNames, paramValues, ssiProcessingState)) {
+        // Turn back on output and mark the branch
+        state.processConditionalCommandsOnly = false;
+        state.branchTaken = true;
+      }
+      else {
+        // Do not process this branch
+        state.processConditionalCommandsOnly = true;
+        state.branchTaken = false;
+      }
+    }
+    else if ("else".equalsIgnoreCase(commandName)) {
+      // No need to even execute if we are nested in
+      // a false branch
+      if (state.nestingCount > 0) return lastModified;
+      // If we've already taken another branch then
+      // disable output otherwise enable it.
+      state.processConditionalCommandsOnly = state.branchTaken;
+      // And in any case, it's safe to say a branch
+      // has been taken.
+      state.branchTaken = true;
+    }
+    else if ("endif".equalsIgnoreCase(commandName)) {
+      // If we are nested inside a false branch then pop out
+      // one level on the nesting count
+      if (state.nestingCount > 0) {
+        state.nestingCount--;
+        return lastModified;
+      }
+      // Turn output back on
+      state.processConditionalCommandsOnly = false;
+      // Reset the branch status for any outer if blocks,
+      // since clearly we took a branch to have gotten here
+      // in the first place.
+      state.branchTaken = true;
+    }
+    else {
+      throw new SsiStopProcessingException();
+    }
+    return lastModified;
+  }
+
+  /**
+   * Retrieves the expression from the specified arguments and performs the necessary evaluation steps.
+   */
+  private static boolean evaluateArguments(@NotNull List<String> names, @NotNull String[] values, @NotNull SsiProcessingState ssiProcessingState) {
+    String expression = "expr".equalsIgnoreCase(names.get(0)) ? values[0] : null;
+    if (expression == null) {
+      throw new SsiStopProcessingException();
+    }
+    try {
+      return new ExpressionParseTree(expression, ssiProcessingState).evaluateTree();
+    }
+    catch (ParseException e) {
+      throw new SsiStopProcessingException();
+    }
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiExternalResolver.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiExternalResolver.java
new file mode 100644 (file)
index 0000000..2f4b4d9
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.builtInWebServer.ssi;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import gnu.trove.THashMap;
+import io.netty.handler.codec.http.HttpRequest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.builtInWebServer.WebServerPathToFileManager;
+
+import java.util.Collection;
+import java.util.Map;
+
+public final class SsiExternalResolver {
+  private final String[] VARIABLE_NAMES = {"AUTH_TYPE", "CONTENT_LENGTH",
+    "CONTENT_TYPE", "DOCUMENT_NAME", "DOCUMENT_URI",
+    "GATEWAY_INTERFACE", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING",
+    "HTTP_ACCEPT_LANGUAGE", "HTTP_CONNECTION", "HTTP_HOST",
+    "HTTP_REFERER", "HTTP_USER_AGENT", "PATH_INFO", "PATH_TRANSLATED",
+    "QUERY_STRING", "QUERY_STRING_UNESCAPED", "REMOTE_ADDR",
+    "REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", "REQUEST_METHOD",
+    "REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_ADDR",
+    "SERVER_NAME", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE",
+    "UNIQUE_ID"};
+
+  private final Project project;
+  private final HttpRequest request;
+
+  private Map<String, String> variables = new THashMap<String, String>();
+  private final String parentPath;
+  @NotNull private final VirtualFile parentFile;
+
+  public SsiExternalResolver(@NotNull Project project,
+                             @NotNull HttpRequest request,
+                             @NotNull String parentPath,
+                             @NotNull VirtualFile parentFile) {
+    this.project = project;
+    this.request = request;
+    this.parentPath = parentPath;
+    this.parentFile = parentFile;
+  }
+
+  public void addVariableNames(@NotNull Collection<String> variableNames) {
+    for (String variableName : VARIABLE_NAMES) {
+      String variableValue = getVariableValue(variableName);
+      if (variableValue != null) {
+        variableNames.add(variableName);
+      }
+    }
+  }
+
+  public void setVariableValue(@NotNull String name, String value) {
+    variables.put(name, value);
+  }
+
+  public String getVariableValue(@NotNull String name) {
+    String value = variables.get(name);
+    return value == null ? request.headers().get(name) : value;
+  }
+
+  @Nullable
+  public VirtualFile findFile(@NotNull String originalPath, boolean virtual) {
+    String path = FileUtil.toCanonicalPath(originalPath, '/');
+    if (!virtual) {
+      return parentFile.findFileByRelativePath(path);
+    }
+
+    path = path.charAt(0) == '/' ? path : (parentPath + '/' + path);
+    return WebServerPathToFileManager.getInstance(project).get(path);
+  }
+
+  public long getFileLastModified(String path, boolean virtual) {
+    VirtualFile file = findFile(path, virtual);
+    return file == null ? 0 : file.getTimeStamp();
+  }
+
+  public long getFileSize(@NotNull String path, boolean virtual) {
+    VirtualFile file = findFile(path, virtual);
+    return file == null ? -1 : file.getLength();
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiFsize.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiFsize.java
new file mode 100644 (file)
index 0000000..1d43611
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jetbrains.builtInWebServer.ssi;
+
+import io.netty.buffer.ByteBufUtf8Writer;
+import org.jetbrains.annotations.NotNull;
+
+import java.text.DecimalFormat;
+import java.util.List;
+
+/**
+ * Implements the Server-side #fsize command
+ *
+ * @author Bip Thelin
+ * @author Paul Speed
+ * @author Dan Sandberg
+ * @author David Becker
+ */
+final class SsiFsize implements SsiCommand {
+  private static final int ONE_KILOBYTE = 1024;
+  private static final int ONE_MEGABYTE = 1024 * 1024;
+
+  @Override
+  public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+    long lastModified = 0;
+    String configErrMsg = state.configErrorMessage;
+    for (int i = 0; i < paramNames.size(); i++) {
+      String paramName = paramNames.get(i);
+      String paramValue = paramValues[i];
+      String substitutedValue = state.substituteVariables(paramValue);
+      if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) {
+        boolean virtual = paramName.equalsIgnoreCase("virtual");
+        lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual);
+        writer.write(formatSize(state.ssiExternalResolver.getFileSize(substitutedValue, virtual), state.configSizeFmt));
+      }
+      else {
+        SsiProcessor.LOG.info("#fsize--Invalid attribute: " + paramName);
+        writer.write(configErrMsg);
+      }
+    }
+    return lastModified;
+  }
+
+  // We try to mimic Apache here, as we do everywhere
+  // All the 'magic' numbers are from the util_script.c Apache source file.
+  private static String formatSize(long size, @NotNull String format) {
+    if (format.equalsIgnoreCase("bytes")) {
+      return new DecimalFormat("#,##0").format(size);
+    }
+
+    String result;
+    if (size == 0) {
+      result = "0k";
+    }
+    else if (size < ONE_KILOBYTE) {
+      result = "1k";
+    }
+    else if (size < ONE_MEGABYTE) {
+      result = Long.toString((size + 512) / ONE_KILOBYTE) + "k";
+    }
+    else if (size < 99 * ONE_MEGABYTE) {
+      result = new DecimalFormat("0.0M").format(size / (double)ONE_MEGABYTE);
+    }
+    else {
+      result = Long.toString((size + (529 * ONE_KILOBYTE)) / ONE_MEGABYTE) + "M";
+    }
+
+    int charsToAdd = 5 - result.length();
+    if (charsToAdd < 0) {
+      throw new IllegalArgumentException("Num chars can't be negative");
+    }
+
+    if (charsToAdd == 0) {
+      return result;
+    }
+
+    StringBuilder buf = new StringBuilder();
+    for (int i = 0; i < charsToAdd; i++) {
+      buf.append(' ');
+    }
+    return buf.append(result).toString();
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessingState.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessingState.java
new file mode 100644 (file)
index 0000000..0d98216
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.builtInWebServer.ssi;
+
+import com.google.common.escape.Escaper;
+import com.google.common.html.HtmlEscapers;
+import com.google.common.net.PercentEscaper;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class SsiProcessingState {
+  protected static final String DEFAULT_CONFIG_ERR_MSG = "[an error occurred while processing this directive]";
+  protected static final String DEFAULT_CONFIG_TIME_FMT = "%A, %d-%b-%Y %T %Z";
+  protected static final String DEFAULT_CONFIG_SIZE_FMT = "abbrev";
+  // encode only the same characters that apache does
+  protected static final Escaper urlEscaper = new PercentEscaper(",:-_.*/!~'()", false);
+  protected String configErrorMessage = DEFAULT_CONFIG_ERR_MSG;
+  protected String configTimeFmt = DEFAULT_CONFIG_TIME_FMT;
+  protected String configSizeFmt = DEFAULT_CONFIG_SIZE_FMT;
+  protected final SsiExternalResolver ssiExternalResolver;
+  protected final long lastModifiedDate;
+  protected Strftime strftime;
+  protected final SsiConditionalState conditionalState = new SsiConditionalState();
+
+  private boolean alreadySet;
+
+  public SsiProcessingState(@NotNull SsiExternalResolver ssiExternalResolver, long lastModifiedDate) {
+    this.ssiExternalResolver = ssiExternalResolver;
+    this.lastModifiedDate = lastModifiedDate;
+    setConfigTimeFormat(DEFAULT_CONFIG_TIME_FMT, true);
+  }
+
+  static class SsiConditionalState {
+    /**
+     * Set to true if the current conditional has already been completed, i.e.:
+     * a branch was taken.
+     */
+    boolean branchTaken = false;
+    /**
+     * Counts the number of nested false branches.
+     */
+    int nestingCount = 0;
+    /**
+     * Set to true if only conditional commands ( if, elif, else, endif )
+     * should be processed.
+     */
+    boolean processConditionalCommandsOnly = false;
+  }
+
+  public void setConfigTimeFormat(String configTimeFmt, boolean fromConstructor) {
+    this.configTimeFmt = configTimeFmt;
+    this.strftime = new Strftime(configTimeFmt, Locale.US);
+    setDateVariables(fromConstructor);
+  }
+
+  public String getVariableValue(String variableName) {
+    return getVariableValue(variableName, "none");
+  }
+
+  public String getVariableValue(@NotNull String variableName, String encoding) {
+    String variableValue = ssiExternalResolver.getVariableValue(variableName);
+    return variableValue == null ? null : encode(variableValue, encoding);
+  }
+
+  /**
+   * Applies variable substitution to the specified String and returns the
+   * new resolved string.
+   */
+  @SuppressWarnings("AssignmentToForLoopParameter")
+  public String substituteVariables(String val) {
+    // If it has no references or HTML entities then no work
+    // need to be done
+    if (val.indexOf('$') < 0 && val.indexOf('&') < 0) {
+      return val;
+    }
+
+    val = val.replace("&lt;", "<");
+    val = val.replace("&gt;", ">");
+    val = val.replace("&quot;", "\"");
+    val = val.replace("&amp;", "&");
+
+    StringBuilder sb = new StringBuilder(val);
+    int charStart = sb.indexOf("&#");
+    while (charStart > -1) {
+      int charEnd = sb.indexOf(";", charStart);
+      if (charEnd > -1) {
+        char c = (char)Integer.parseInt(
+          sb.substring(charStart + 2, charEnd));
+        sb.delete(charStart, charEnd + 1);
+        sb.insert(charStart, c);
+        charStart = sb.indexOf("&#");
+      }
+      else {
+        break;
+      }
+    }
+
+    for (int i = 0; i < sb.length(); ) {
+      // Find the next $
+      for (; i < sb.length(); i++) {
+        if (sb.charAt(i) == '$') {
+          i++;
+          break;
+        }
+      }
+      if (i == sb.length()) break;
+      // Check to see if the $ is escaped
+      if (i > 1 && sb.charAt(i - 2) == '\\') {
+        sb.deleteCharAt(i - 2);
+        i--;
+        continue;
+      }
+      int nameStart = i;
+      int start = i - 1;
+      int end;
+      int nameEnd;
+      char endChar = ' ';
+      // Check for {} wrapped var
+      if (sb.charAt(i) == '{') {
+        nameStart++;
+        endChar = '}';
+      }
+      // Find the end of the var reference
+      for (; i < sb.length(); i++) {
+        if (sb.charAt(i) == endChar) break;
+      }
+      end = i;
+      nameEnd = end;
+      if (endChar == '}') end++;
+      // We should now have enough to extract the var name
+      String varName = sb.substring(nameStart, nameEnd);
+      String value = getVariableValue(varName);
+      if (value == null) value = "";
+      // Replace the var name with its value
+      sb.replace(start, end, value);
+      // Start searching for the next $ after the value
+      // that was just substituted.
+      i = start + value.length();
+    }
+    return sb.toString();
+  }
+
+  protected void setDateVariables(boolean fromConstructor) {
+    if (fromConstructor && alreadySet) {
+      return;
+    }
+
+    alreadySet = true;
+    Date date = new Date();
+    TimeZone timeZone = TimeZone.getTimeZone("GMT");
+    ssiExternalResolver.setVariableValue("DATE_GMT", formatDate(date, timeZone));
+    ssiExternalResolver.setVariableValue("DATE_LOCAL", formatDate(date, null));
+    ssiExternalResolver.setVariableValue("LAST_MODIFIED", formatDate(new Date(lastModifiedDate), null));
+  }
+
+  @NotNull
+  protected String formatDate(@NotNull Date date, @Nullable TimeZone timeZone) {
+    if (timeZone == null) {
+      return strftime.format(date);
+    }
+    else {
+      TimeZone oldTimeZone = strftime.getTimeZone();
+      strftime.setTimeZone(timeZone);
+      String retVal = strftime.format(date);
+      strftime.setTimeZone(oldTimeZone);
+      return retVal;
+    }
+  }
+
+  @NotNull
+  protected String encode(@NotNull String value, @NotNull String encoding) {
+    if (encoding.equalsIgnoreCase("url")) {
+      return urlEscaper.escape(value);
+    }
+    else if (encoding.equalsIgnoreCase("none")) {
+      return value;
+    }
+    else if (encoding.equalsIgnoreCase("entity")) {
+      return HtmlEscapers.htmlEscaper().escape(value);
+    }
+    else {
+      throw new IllegalArgumentException("Unknown encoding: " + encoding);
+    }
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessor.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiProcessor.java
new file mode 100644 (file)
index 0000000..42f1947
--- /dev/null
@@ -0,0 +1,440 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.builtInWebServer.ssi;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.SmartList;
+import gnu.trove.THashMap;
+import io.netty.buffer.ByteBufUtf8Writer;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+public class SsiProcessor {
+  static final Logger LOG = Logger.getInstance(SsiProcessor.class);
+
+  protected static final String COMMAND_START = "<!--#";
+  protected static final String COMMAND_END = "-->";
+
+  protected final Map<String, SsiCommand> commands = new THashMap<String, SsiCommand>();
+
+  @SuppressWarnings("SpellCheckingInspection")
+  public SsiProcessor(boolean allowExec) {
+    commands.put("config", new SsiCommand() {
+      @Override
+      public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+        for (int i = 0; i < paramNames.size(); i++) {
+          String paramName = paramNames.get(i);
+          String paramValue = paramValues[i];
+          String substitutedValue = state.substituteVariables(paramValue);
+          if (paramName.equalsIgnoreCase("errmsg")) {
+            state.configErrorMessage = substitutedValue;
+          }
+          else if (paramName.equalsIgnoreCase("sizefmt")) {
+            state.configSizeFmt = substitutedValue;
+          }
+          else if (paramName.equalsIgnoreCase("timefmt")) {
+            state.setConfigTimeFormat(substitutedValue, false);
+          }
+          else {
+            LOG.info("#config--Invalid attribute: " + paramName);
+            // We need to fetch this value each time, since it may change during the loop
+            writer.write(state.configErrorMessage);
+          }
+        }
+        return 0;
+      }
+    });
+    commands.put("echo", new SsiCommand() {
+      @Override
+      public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+        String encoding = "entity";
+        String originalValue = null;
+        String errorMessage = state.configErrorMessage;
+        for (int i = 0; i < paramNames.size(); i++) {
+          String paramName = paramNames.get(i);
+          String paramValue = paramValues[i];
+          if (paramName.equalsIgnoreCase("var")) {
+            originalValue = paramValue;
+          }
+          else if (paramName.equalsIgnoreCase("encoding")) {
+            if (paramValue.equalsIgnoreCase("url") || paramValue.equalsIgnoreCase("entity") || paramValue.equalsIgnoreCase("none")) {
+              encoding = paramValue;
+            }
+            else {
+              LOG.info("#echo--Invalid encoding: " + paramValue);
+              writer.write(errorMessage);
+            }
+          }
+          else {
+            LOG.info("#echo--Invalid attribute: " + paramName);
+            writer.write(errorMessage);
+          }
+        }
+        assert originalValue != null;
+        String variableValue = state.getVariableValue(originalValue, encoding);
+        writer.write(variableValue == null ? "(none)" : variableValue);
+        return System.currentTimeMillis();
+      }
+    });
+    //noinspection StatementWithEmptyBody
+    if (allowExec) {
+      // commands.put("exec", new SsiExec());
+    }
+    commands.put("include", new SsiCommand() {
+      @Override
+      public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+        long lastModified = 0;
+        String configErrorMessage = state.configErrorMessage;
+        for (int i = 0; i < paramNames.size(); i++) {
+          String paramName = paramNames.get(i);
+          if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) {
+            String substitutedValue = state.substituteVariables(paramValues[i]);
+            try {
+              boolean virtual = paramName.equalsIgnoreCase("virtual");
+              lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual);
+              VirtualFile file = state.ssiExternalResolver.findFile(substitutedValue, virtual);
+              if (file == null) {
+                LOG.warn("#include-- Couldn't find file: " + substitutedValue);
+                return 0;
+              }
+
+              InputStream in = file.getInputStream();
+              try {
+                writer.write(in, (int)file.getLength());
+              }
+              finally {
+                in.close();
+              }
+            }
+            catch (IOException e) {
+              LOG.warn("#include--Couldn't include file: " + substitutedValue, e);
+              writer.write(configErrorMessage);
+            }
+          }
+          else {
+            LOG.info("#include--Invalid attribute: " + paramName);
+            writer.write(configErrorMessage);
+          }
+        }
+        return lastModified;
+      }
+    });
+    commands.put("flastmod", new SsiCommand() {
+      @Override
+      public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+        long lastModified = 0;
+        String configErrMsg = state.configErrorMessage;
+        for (int i = 0; i < paramNames.size(); i++) {
+          String paramName = paramNames.get(i);
+          String paramValue = paramValues[i];
+          String substitutedValue = state.substituteVariables(paramValue);
+          if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) {
+            boolean virtual = paramName.equalsIgnoreCase("virtual");
+            lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual);
+            Strftime strftime = new Strftime(state.configTimeFmt, Locale.US);
+            writer.write(strftime.format(new Date(lastModified)));
+          }
+          else {
+            LOG.info("#flastmod--Invalid attribute: " + paramName);
+            writer.write(configErrMsg);
+          }
+        }
+        return lastModified;
+      }
+    });
+    commands.put("fsize", new SsiFsize());
+    commands.put("printenv", new SsiCommand() {
+      @Override
+      public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+        long lastModified = 0;
+        // any arguments should produce an error
+        if (paramNames.isEmpty()) {
+          Set<String> variableNames = new LinkedHashSet<String>();
+          //These built-in variables are supplied by the mediator ( if not over-written by the user ) and always exist
+          variableNames.add("DATE_GMT");
+          variableNames.add("DATE_LOCAL");
+          variableNames.add("LAST_MODIFIED");
+          state.ssiExternalResolver.addVariableNames(variableNames);
+          for (String variableName : variableNames) {
+            String variableValue = state.getVariableValue(variableName);
+            // This shouldn't happen, since all the variable names must have values
+            if (variableValue == null) {
+              variableValue = "(none)";
+            }
+            writer.write(variableName);
+            writer.write('=');
+            writer.write(variableValue);
+            writer.write('\n');
+            lastModified = System.currentTimeMillis();
+          }
+        }
+        else {
+          writer.write(state.configErrorMessage);
+        }
+        return lastModified;
+      }
+    });
+    commands.put("set", new SsiCommand() {
+      @Override
+      public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
+        long lastModified = 0;
+        String errorMessage = state.configErrorMessage;
+        String variableName = null;
+        for (int i = 0; i < paramNames.size(); i++) {
+          String paramName = paramNames.get(i);
+          String paramValue = paramValues[i];
+          if (paramName.equalsIgnoreCase("var")) {
+            variableName = paramValue;
+          }
+          else if (paramName.equalsIgnoreCase("value")) {
+            if (variableName != null) {
+              String substitutedValue = state.substituteVariables(paramValue);
+              state.ssiExternalResolver.setVariableValue(variableName, substitutedValue);
+              lastModified = System.currentTimeMillis();
+            }
+            else {
+              LOG.info("#set--no variable specified");
+              writer.write(errorMessage);
+              throw new SsiStopProcessingException();
+            }
+          }
+          else {
+            LOG.info("#set--Invalid attribute: " + paramName);
+            writer.write(errorMessage);
+            throw new SsiStopProcessingException();
+          }
+        }
+        return lastModified;
+      }
+    });
+
+    SsiConditional ssiConditional = new SsiConditional();
+    commands.put("if", ssiConditional);
+    commands.put("elif", ssiConditional);
+    commands.put("endif", ssiConditional);
+    commands.put("else", ssiConditional);
+  }
+
+  /**
+   * @return the most current modified date resulting from any SSI commands
+   */
+  public long process(@NotNull SsiExternalResolver ssiExternalResolver, @NotNull String fileContents, long lastModifiedDate, @NotNull ByteBufUtf8Writer writer) {
+    SsiProcessingState ssiProcessingState = new SsiProcessingState(ssiExternalResolver, lastModifiedDate);
+    int index = 0;
+    boolean inside = false;
+    StringBuilder command = new StringBuilder();
+    try {
+      while (index < fileContents.length()) {
+        char c = fileContents.charAt(index);
+        if (inside) {
+          if (c == COMMAND_END.charAt(0) && charCmp(fileContents, index, COMMAND_END)) {
+            inside = false;
+            index += COMMAND_END.length();
+            String commandName = parseCommand(command);
+            if (LOG.isDebugEnabled()) {
+              LOG.debug("SSIProcessor.process -- processing command: " + commandName);
+            }
+            List<String> paramNames = parseParamNames(command, commandName.length());
+            String[] paramValues = parseParamValues(command, commandName.length(), paramNames.size());
+            // We need to fetch this value each time, since it may change during the loop
+            String configErrMsg = ssiProcessingState.configErrorMessage;
+            SsiCommand ssiCommand = commands.get(commandName.toLowerCase(Locale.ENGLISH));
+            String errorMessage = null;
+            if (ssiCommand == null) {
+              errorMessage = "Unknown command: " + commandName;
+            }
+            else if (paramValues == null) {
+              errorMessage = "Error parsing directive parameters.";
+            }
+            else if (paramNames.size() != paramValues.length) {
+              errorMessage = "Parameter names count does not match parameter values count on command: " + commandName;
+            }
+            else {
+              // don't process the command if we are processing conditional commands only and the command is not conditional
+              if (!ssiProcessingState.conditionalState.processConditionalCommandsOnly || ssiCommand instanceof SsiConditional) {
+                long newLastModified = ssiCommand.process(ssiProcessingState, commandName, paramNames, paramValues, writer);
+                if (newLastModified > lastModifiedDate) {
+                  lastModifiedDate = newLastModified;
+                }
+              }
+            }
+            if (errorMessage != null) {
+              LOG.warn(errorMessage);
+              writer.write(configErrMsg);
+            }
+          }
+          else {
+            command.append(c);
+            index++;
+          }
+        }
+        else if (c == COMMAND_START.charAt(0) && charCmp(fileContents, index, COMMAND_START)) {
+          inside = true;
+          index += COMMAND_START.length();
+          command.setLength(0);
+        }
+        else {
+          if (!ssiProcessingState.conditionalState.processConditionalCommandsOnly) {
+            writer.write(c);
+          }
+          index++;
+        }
+      }
+    }
+    catch (SsiStopProcessingException e) {
+      //If we are here, then we have already stopped processing, so all is good
+    }
+    return lastModifiedDate;
+  }
+
+  @NotNull
+  protected List<String> parseParamNames(@NotNull StringBuilder command, int start) {
+    int bIdx = start;
+    List<String> values = new SmartList<String>();
+    boolean inside = false;
+    StringBuilder builder = new StringBuilder();
+    while (bIdx < command.length()) {
+      if (inside) {
+        while (bIdx < command.length() && command.charAt(bIdx) != '=') {
+          builder.append(command.charAt(bIdx));
+          bIdx++;
+        }
+
+        values.add(builder.toString());
+        builder.setLength(0);
+        inside = false;
+        int quotes = 0;
+        boolean escaped = false;
+        for (; bIdx < command.length() && quotes != 2; bIdx++) {
+          char c = command.charAt(bIdx);
+          // Need to skip escaped characters
+          if (c == '\\' && !escaped) {
+            escaped = true;
+            continue;
+          }
+          if (c == '"' && !escaped) {
+            quotes++;
+          }
+          escaped = false;
+        }
+      }
+      else {
+        while (bIdx < command.length() && isSpace(command.charAt(bIdx))) {
+          bIdx++;
+        }
+        if (bIdx >= command.length()) {
+          break;
+        }
+        inside = true;
+      }
+    }
+    return values;
+  }
+
+  @SuppressWarnings("AssignmentToForLoopParameter")
+  protected String[] parseParamValues(@NotNull StringBuilder command, int start, int count) {
+    int valueIndex = 0;
+    boolean inside = false;
+    String[] values = new String[count];
+    StringBuilder builder = new StringBuilder();
+    char endQuote = 0;
+    for (int bIdx = start; bIdx < command.length(); bIdx++) {
+      if (!inside) {
+        while (bIdx < command.length() && !isQuote(command.charAt(bIdx))) {
+          bIdx++;
+        }
+        if (bIdx >= command.length()) {
+          break;
+        }
+        inside = true;
+        endQuote = command.charAt(bIdx);
+      }
+      else {
+        boolean escaped = false;
+        for (; bIdx < command.length(); bIdx++) {
+          char c = command.charAt(bIdx);
+          // Check for escapes
+          if (c == '\\' && !escaped) {
+            escaped = true;
+            continue;
+          }
+          // If we reach the other " then stop
+          if (c == endQuote && !escaped) {
+            break;
+          }
+          // Since parsing of attributes and var
+          // substitution is done in separate places,
+          // we need to leave escape in the string
+          if (c == '$' && escaped) {
+            builder.append('\\');
+          }
+          escaped = false;
+          builder.append(c);
+        }
+        // If we hit the end without seeing a quote
+        // the signal an error
+        if (bIdx == command.length()) {
+          return null;
+        }
+        values[valueIndex++] = builder.toString();
+        builder.setLength(0);
+        inside = false;
+      }
+    }
+    return values;
+  }
+
+  @NotNull
+  private String parseCommand(@NotNull StringBuilder instruction) {
+    int firstLetter = -1;
+    int lastLetter = -1;
+    for (int i = 0; i < instruction.length(); i++) {
+      char c = instruction.charAt(i);
+      if (Character.isLetter(c)) {
+        if (firstLetter == -1) {
+          firstLetter = i;
+        }
+        lastLetter = i;
+      }
+      else if (isSpace(c)) {
+        if (lastLetter > -1) {
+          break;
+        }
+      }
+      else {
+        break;
+      }
+    }
+    return firstLetter == -1 ? "" : instruction.substring(firstLetter, lastLetter + 1);
+  }
+
+  protected boolean charCmp(String buf, int index, String command) {
+    return buf.regionMatches(index, command, 0, command.length());
+  }
+
+
+  protected boolean isSpace(char c) {
+    return c == ' ' || c == '\n' || c == '\t' || c == '\r';
+  }
+
+  protected boolean isQuote(char c) {
+    return c == '\'' || c == '\"' || c == '`';
+  }
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiStopProcessingException.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/SsiStopProcessingException.java
new file mode 100644 (file)
index 0000000..fd02896
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jetbrains.builtInWebServer.ssi;
+
+
+/**
+ * Exception used to tell SSIProcessor that it should stop processing SSI
+ * commands. This is used to mimic the Apache behavior in #set with invalid
+ * attributes.
+ *
+ * @author Paul Speed
+ * @author Dan Sandberg
+ */
+public class SsiStopProcessingException extends RuntimeException {
+}
\ No newline at end of file
diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/Strftime.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/ssi/Strftime.java
new file mode 100644 (file)
index 0000000..80edb6b
--- /dev/null
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jetbrains.builtInWebServer.ssi;
+
+import gnu.trove.THashMap;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Converts dates to strings using the same format specifiers as strftime
+ * <p/>
+ * Note: This does not mimic strftime perfectly.  Certain strftime commands,
+ * are not supported, and will convert as if they were literals.
+ * <p/>
+ * Certain complicated commands, like those dealing with the week of the year
+ * probably don't have exactly the same behavior as strftime.
+ * <p/>
+ * These limitations are due to use SimpleDateTime.  If the conversion was done
+ * manually, all these limitations could be eliminated.
+ * <p/>
+ * The interface looks like a subset of DateFormat.  Maybe someday someone will make this class
+ * extend DateFormat.
+ *
+ * @author Bip Thelin
+ * @author Dan Sandberg
+ */
+@SuppressWarnings("SpellCheckingInspection")
+public class Strftime {
+  protected static final Map<String, String> translate = new THashMap<String, String>();
+  protected final SimpleDateFormat simpleDateFormat;
+
+  static {
+    translate.put("a", "EEE");
+    translate.put("A", "EEEE");
+    translate.put("b", "MMM");
+    translate.put("B", "MMMM");
+    translate.put("c", "EEE MMM d HH:mm:ss yyyy");
+
+    //There's no way to specify the century in SimpleDateFormat.  We don't want to hard-code
+    //20 since this could be wrong for the pre-2000 files.
+    //translate.put("C", "20");
+    translate.put("d", "dd");
+    translate.put("D", "MM/dd/yy");
+    translate.put("e", "dd"); //will show as '03' instead of ' 3'
+    translate.put("F", "yyyy-MM-dd");
+    translate.put("g", "yy");
+    translate.put("G", "yyyy");
+    translate.put("H", "HH");
+    translate.put("h", "MMM");
+    translate.put("I", "hh");
+    translate.put("j", "DDD");
+    translate.put("k", "HH"); //will show as '07' instead of ' 7'
+    translate.put("l", "hh"); //will show as '07' instead of ' 7'
+    translate.put("m", "MM");
+    translate.put("M", "mm");
+    translate.put("n", "\n");
+    translate.put("p", "a");
+    translate.put("P", "a");  //will show as pm instead of PM
+    translate.put("r", "hh:mm:ss a");
+    translate.put("R", "HH:mm");
+    //There's no way to specify this with SimpleDateFormat
+    //translate.put("s","seconds since epoch");
+    translate.put("S", "ss");
+    translate.put("t", "\t");
+    translate.put("T", "HH:mm:ss");
+    //There's no way to specify this with SimpleDateFormat
+    //translate.put("u","day of week ( 1-7 )");
+
+    //There's no way to specify this with SimpleDateFormat
+    //translate.put("U","week in year with first Sunday as first day...");
+
+    translate.put("V", "ww"); //I'm not sure this is always exactly the same
+
+    //There's no way to specify this with SimpleDateFormat
+    //translate.put("W","week in year with first Monday as first day...");
+
+    //There's no way to specify this with SimpleDateFormat
+    //translate.put("w","E");
+    translate.put("X", "HH:mm:ss");
+    translate.put("x", "MM/dd/yy");
+    translate.put("y", "yy");
+    translate.put("Y", "yyyy");
+    translate.put("Z", "z");
+    translate.put("z", "Z");
+    translate.put("%", "%");
+  }
+
+  /**
+   * Create an instance of this date formatting class
+   *
+   * @param origFormat the strftime-style formatting string
+   * @param locale     the locale to use for locale-specific conversions
+   */
+  public Strftime(String origFormat, Locale locale) {
+    String convertedFormat = convertDateFormat(origFormat);
+    simpleDateFormat = new SimpleDateFormat(convertedFormat, locale);
+  }
+
+  /**
+   * Format the date according to the strftime-style string given in the constructor.
+   *
+   * @param date the date to format
+   * @return the formatted date
+   */
+  public String format(Date date) {
+    return simpleDateFormat.format(date);
+  }
+
+  /**
+   * Get the timezone used for formatting conversions
+   *
+   * @return the timezone
+   */
+  public TimeZone getTimeZone() {
+    return simpleDateFormat.getTimeZone();
+  }
+
+  /**
+   * Change the timezone used to format dates
+   *
+   * @see SimpleDateFormat#setTimeZone
+   */
+  public void setTimeZone(TimeZone timeZone) {
+    simpleDateFormat.setTimeZone(timeZone);
+  }
+
+  /**
+   * Search the provided pattern and get the C standard
+   * Date/Time formatting rules and convert them to the
+   * Java equivalent.
+   *
+   * @param pattern The pattern to search
+   * @return The modified pattern
+   */
+  protected String convertDateFormat(String pattern) {
+    boolean inside = false;
+    boolean mark = false;
+    boolean modifiedCommand = false;
+
+    StringBuilder buf = new StringBuilder();
+
+    for (int i = 0; i < pattern.length(); i++) {
+      char c = pattern.charAt(i);
+
+      if (c == '%' && !mark) {
+        mark = true;
+      }
+      else {
+        if (mark) {
+          if (modifiedCommand) {
+            //don't do anything--we just wanted to skip a char
+            modifiedCommand = false;
+            mark = false;
+          }
+          else {
+            inside = translateCommand(buf, pattern, i, inside);
+            //It's a modifier code
+            if (c == 'O' || c == 'E') {
+              modifiedCommand = true;
+            }
+            else {
+              mark = false;
+            }
+          }
+        }
+        else {
+          if (!inside && c != ' ') {
+            //We start a literal, which we need to quote
+            buf.append("'");
+            inside = true;
+          }
+
+          buf.append(c);
+        }
+      }
+    }
+
+    if (buf.length() > 0) {
+      char lastChar = buf.charAt(buf.length() - 1);
+
+      if (lastChar != '\'' && inside) {
+        buf.append('\'');
+      }
+    }
+    return buf.toString();
+  }
+
+  protected String quote(String str, boolean insideQuotes) {
+    String retVal = str;
+    if (!insideQuotes) {
+      retVal = '\'' + retVal + '\'';
+    }
+    return retVal;
+  }
+
+  /**
+   * Try to get the Java Date/Time formatting associated with
+   * the C standard provided.
+   *
+   * @param buf       The buffer
+   * @param pattern   The date/time pattern
+   * @param index     The char index
+   * @param oldInside Flag value
+   * @return True if new is inside buffer
+   */
+  protected boolean translateCommand(StringBuilder buf, String pattern, int index, boolean oldInside) {
+    char firstChar = pattern.charAt(index);
+    boolean newInside = oldInside;
+
+    //O and E are modifiers, they mean to present an alternative representation of the next char
+    //we just handle the next char as if the O or E wasn't there
+    if (firstChar == 'O' || firstChar == 'E') {
+      if (index + 1 < pattern.length()) {
+        newInside = translateCommand(buf, pattern, index + 1, oldInside);
+      }
+      else {
+        buf.append(quote("%" + firstChar, oldInside));
+      }
+    }
+    else {
+      String command = translate.get(String.valueOf(firstChar));
+      //If we don't find a format, treat it as a literal--That's what apache does
+      if (command == null) {
+        buf.append(quote("%" + firstChar, oldInside));
+      }
+      else {
+        //If we were inside quotes, close the quotes
+        if (oldInside) {
+          buf.append('\'');
+        }
+        buf.append(command);
+        newInside = false;
+      }
+    }
+    return newInside;
+  }
+}
index a1c16f1e0460f57231ed75ef0d295864aa5bb4f3..c9323e02b7be35d2ff8bce09c5cb9abd69ea9a17 100644 (file)
@@ -96,7 +96,7 @@ public class FileResponses {
     try {
       long fileLength = raf.length();
       if (request.method() != HttpMethod.HEAD) {
-        HttpHeaders.setContentLength(response, fileLength);
+        HttpHeaderUtil.setContentLength(response, fileLength);
       }
 
       channel.write(response);
index da6479b35fce08e1eae1827f599799c0cbc0548f..788291746672c906cea51fb8d1ceef87467788e1 100644 (file)
@@ -19,6 +19,7 @@ import com.intellij.util.text.CharArrayCharSequence;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.Writer;
 
 public final class ByteBufUtf8Writer extends Writer {
@@ -28,6 +29,10 @@ public final class ByteBufUtf8Writer extends Writer {
     this.buffer = buffer;
   }
 
+  public void write(@NotNull InputStream inputStream, int length) throws IOException {
+    buffer.writeBytes(inputStream, length);
+  }
+
   @Override
   public void write(int c) {
     AbstractByteBuf buffer = ByteBufUtilEx.getBuf(this.buffer);
@@ -53,7 +58,7 @@ public final class ByteBufUtf8Writer extends Writer {
   }
 
   @Override
-  public void write(String str) throws IOException {
+  public void write(String str) {
     ByteBufUtilEx.writeUtf8(buffer, str);
   }