Example #1
0
 private ITemplate templateInstance_() {
   if (!isValid) return NULL_TEMPLATE;
   if (null == templateInstance) {
     try {
       if (Logger.isTraceEnabled()) logger.trace("About to new template instance");
       Class<?> clz = getJavaClass();
       if (Logger.isTraceEnabled()) logger.trace("template java class loaded");
       templateInstance = (TemplateBase) clz.newInstance();
       if (Logger.isTraceEnabled()) logger.trace("template instance generated");
     } catch (RythmException e) {
       throw e;
     } catch (Exception e) {
       throw new RuntimeException("Error load template instance for " + getKey(), e);
     }
   }
   if (!engine().isProdMode()) {
     // check parent class change
     Class<?> c = templateInstance.getClass();
     Class<?> pc = c.getSuperclass();
     if (null != pc && !Modifier.isAbstract(pc.getModifiers())) {
       engine().classes.getByClassName(pc.getName());
     }
   }
   templateInstance.setTemplateClass(this);
   return templateInstance;
 }
Example #2
0
 public static void registerLoggerFactory(ILoggerFactory fact) {
   Logger.registerLoggerFactory(fact);
 }
Example #3
0
/** Define the data structure hold template class/template src/generated java src */
public class TemplateClass {
  private static AtomicLong nextVersion = new AtomicLong();
  private static final ILogger logger = Logger.get(TemplateClass.class);

  /** Store root level template class, e.g. the one that is not an embedded class */
  private TemplateClass root;

  public TemplateClass root() {
    return root;
  }

  private TemplateClass() {}

  private boolean inner = false;

  public static TemplateClass createInnerClass(
      String className, byte[] byteCode, TemplateClass parent) {
    TemplateClass tc = new TemplateClass();
    tc.name = className;
    tc.javaByteCode = byteCode;
    // tc.enhancedByteCode = byteCode;
    tc.inner = true;
    tc.root = parent.root();
    tc.version = parent.version();
    return tc;
  }

  public boolean isInner() {
    return inner;
  }

  private RythmEngine engine = null;

  private RythmEngine engine() {
    return null == engine ? Rythm.engine() : engine;
  }

  /** The fully qualified class name */
  private String name;

  public String name0() {
    return name;
  }

  public String name() {
    // return isInner() ? name : name + "v" + version;
    RythmEngine e = engine();
    String n = (!e.reloadByIncClassVersion() || isInner()) ? name : name + "v" + version;
    return n;
  }

  private long version;

  public long version() {
    return root().version;
  }

  public void setVersion(int v) {
    version = (long) v;
  }

  public TemplateClass extendedTemplateClass;
  private Set<TemplateClass> includedTemplateClasses = new HashSet<TemplateClass>();

  public void addIncludeTemplateClass(TemplateClass tc) {
    includedTemplateClasses.add(tc);
    includeTagTypes.putAll(tc.includeTagTypes);
  }

  public String includeTemplateClassNames = null;
  private static final String NO_INCLUDE_CLASS = "NO_INCLUDE_CLASS";

  public String refreshIncludeTemplateClassNames() {
    if (includedTemplateClasses.size() == 0) {
      includeTemplateClassNames = NO_INCLUDE_CLASS;
      return NO_INCLUDE_CLASS;
    }
    StringBuilder sb = new StringBuilder();
    boolean first = true;
    for (TemplateClass tc : includedTemplateClasses) {
      if (!first) {
        sb.append(",");
      } else {
        first = false;
      }
      sb.append(engine().resourceManager.getFullTagName(tc));
    }
    includeTemplateClassNames = sb.toString();
    return sb.toString();
  }

  private Map<String, String> includeTagTypes = new HashMap<String, String>();

  public void setTagType(String tagName, String type) {
    includeTagTypes.put(tagName, type);
  }

  public String getTagType(String tagName) {
    return includeTagTypes.get(tagName);
  }

  public boolean returnObject(String tagName) {
    String retType = includeTagTypes.get(tagName);
    if (null != retType) {
      return !"void".equals(retType);
    }
    if (null != extendedTemplateClass) return extendedTemplateClass.returnObject(tagName);
    return true;
  }

  public String serializeIncludeTagTypes() {
    if (includeTagTypes.isEmpty()) return "";
    StringBuilder sb = new StringBuilder();
    boolean empty = true;
    for (String tagName : includeTagTypes.keySet()) {
      if (!empty) sb.append(";");
      else empty = false;
      sb.append(tagName).append(":").append(includeTagTypes.get(tagName));
    }
    return sb.toString();
  }

  public void deserializeIncludeTagTypes(String s) {
    includeTagTypes = new HashMap<String, String>();
    if (S.isEmpty(s)) return;
    String[] sa = s.split(";");
    for (String s0 : sa) {
      String[] sa0 = s0.split(":");
      if (sa0.length != 2)
        throw new IllegalArgumentException("Unknown include tag types string: " + s);
      includeTagTypes.put(sa0[0], sa0[1]);
    }
  }
  /** If not null then this template is a tag */
  public String tagName() {
    return null != templateResource ? templateResource.tagName() : null;
  }

  private transient String fullName;

  public void setFullName(String fn) {
    fullName = fn;
  }

  public String getFullName() {
    if (null == fullName) return tagName();
    return fullName;
  }
  /** the template resource */
  public ITemplateResource templateResource;
  /** The template source */
  public String getTemplateSource() {
    return getTemplateSource(false);
  }

  public String getTemplateSource(boolean includeRoot) {
    if (null != templateResource) return templateResource.asTemplateContent();
    if (!includeRoot) return "";
    TemplateClass parent = root;
    while ((null != parent) && parent.isInner()) {
      parent = parent.root;
    }
    return null == parent ? "" : parent.getTemplateSource();
  }
  /** Is this template resource coming from a literal String or from a loaded resource like file */
  public boolean isStringTemplate() {
    return templateResource instanceof StringTemplateResource;
  }
  /** The Java source */
  public String javaSource;
  /** The compiled byteCode */
  public byte[] javaByteCode;
  /** The enhanced byteCode */
  public byte[] enhancedByteCode;
  /** Store a list of import path, i.e. those imports ends with ".*" */
  public Set<String> importPaths;
  /** The in JVM loaded class */
  public Class<ITemplate> javaClass;
  /** The in JVM loaded package */
  public Package javaPackage;
  /** Is this class compiled */
  boolean compiled;
  /** Signatures checksum */
  public int sigChecksum;

  /** Mark if this is a valid Rythm Template */
  public boolean isValid = true;

  /**
   * CodeBuilder to generate java source code
   *
   * <p>Could be used to merge state into including template class codeBuilder
   */
  public CodeBuilder codeBuilder;

  /** Indicate this template class represent a simple template */
  public boolean simpleTemplate;

  /** The ITemplate instance */
  public TemplateBase templateInstance;

  /** specify the dialect for the template */
  private transient IDialect dialect;

  private TemplateClass(RythmEngine engine) {
    this.engine = null == engine ? null : engine.isSingleton() ? null : engine;
  }

  /**
   * Construct a TemplateClass instance using template source file
   *
   * @param file the template source file
   */
  public TemplateClass(File file, RythmEngine engine) {
    this(engine.resourceManager.get(file), engine);
  }

  /**
   * Construct a TemplateClass instance using template source content or file path
   *
   * @param template
   */
  public TemplateClass(String template, RythmEngine engine) {
    this(engine.resourceManager.get(template), engine);
  }

  public TemplateClass(ITemplateResource resource, RythmEngine engine) {
    this(resource, engine, false);
  }

  public TemplateClass(ITemplateResource resource, RythmEngine engine, IDialect dialect) {
    this(resource, engine, false, dialect);
  }

  public TemplateClass(ITemplateResource resource, RythmEngine engine, boolean noRefresh) {
    this(engine);
    if (null == resource) throw new NullPointerException();
    templateResource = resource;
    if (resource instanceof StringTemplateResource) {
      simpleTemplate = true;
    }
    if (!noRefresh) refresh();
  }

  public TemplateClass(
      ITemplateResource resource, RythmEngine engine, boolean noRefresh, IDialect dialect) {
    this(engine);
    if (null == resource) throw new NullPointerException();
    templateResource = resource;
    if (resource instanceof StringTemplateResource) {
      simpleTemplate = true;
    }
    this.dialect = dialect;
    if (!noRefresh) refresh();
  }

  /**
   * Return string representation of the template
   *
   * @return
   */
  public Object getKey() {
    return null == templateResource ? name() : templateResource.getKey();
  }

  @SuppressWarnings("unchecked")
  private Class<?> getJavaClass() throws Exception {
    Class<?> c = engine().classLoader.loadClass(name(), true);
    if (null == javaClass) javaClass = (Class<ITemplate>) c;
    return c;
  }

  private static final ITemplate NULL_TEMPLATE =
      new TemplateBase() {
        @Override
        public ITemplate cloneMe(RythmEngine engine, ITemplate caller) {
          return null;
        }
      };

  private ITemplate templateInstance_() {
    if (!isValid) return NULL_TEMPLATE;
    if (null == templateInstance) {
      try {
        if (Logger.isTraceEnabled()) logger.trace("About to new template instance");
        Class<?> clz = getJavaClass();
        if (Logger.isTraceEnabled()) logger.trace("template java class loaded");
        templateInstance = (TemplateBase) clz.newInstance();
        if (Logger.isTraceEnabled()) logger.trace("template instance generated");
      } catch (RythmException e) {
        throw e;
      } catch (Exception e) {
        throw new RuntimeException("Error load template instance for " + getKey(), e);
      }
    }
    if (!engine().isProdMode()) {
      // check parent class change
      Class<?> c = templateInstance.getClass();
      Class<?> pc = c.getSuperclass();
      if (null != pc && !Modifier.isAbstract(pc.getModifiers())) {
        engine().classes.getByClassName(pc.getName());
      }
    }
    templateInstance.setTemplateClass(this);
    return templateInstance;
  }

  public ITemplate asTemplate() {
    if (null == name || engine().mode.isDev()) refresh();
    return templateInstance_().cloneMe(engine(), null);
  }

  public ITemplate asTemplate(ITemplate caller) {
    return templateInstance_().cloneMe(engine(), caller);
  }

  private boolean refreshing = false;
  private boolean compiling = false;
  private Object refreshLock = new Object();

  private boolean refreshing() {
    synchronized (refreshLock) {
      return refreshing || compiling;
    }
  }

  private void refreshing(boolean b) {
    synchronized (refreshLock) {
      refreshing = b;
    }
  }

  private void addVersion() {
    RythmEngine e = engine();
    if (!e.reloadByIncClassVersion()) return;
    TemplateClassManager tcc = engine().classes;
    tcc.clsNameIdx.remove(name());
    // List<TemplateClass> allEmbedded = tcc.getEmbeddedClasses(name0());
    version = nextVersion.getAndIncrement();
    tcc.clsNameIdx.put(name(), this);
  }

  public boolean refresh() {
    return refresh(false);
  }

  public void buildSourceCode(String includingClassName) {
    long start = System.currentTimeMillis();
    addVersion();
    importPaths = new HashSet<String>();
    // Possible bug here?
    if (null != codeBuilder) codeBuilder.clear();
    codeBuilder =
        new CodeBuilder(
            templateResource.asTemplateContent(), name(), tagName(), this, engine, dialect);
    codeBuilder.includingCName = includingClassName;
    codeBuilder.build();
    extendedTemplateClass = codeBuilder.getExtendedTemplateClass();
    javaSource = codeBuilder.toString();
    if (logger.isTraceEnabled()) {
      logger.trace(
          "%s ms to generate java source for template: %s",
          System.currentTimeMillis() - start, getKey());
    }
  }

  public void buildSourceCode() {
    long start = System.currentTimeMillis();
    addVersion();
    importPaths = new HashSet<String>();
    // Possible bug here?
    if (null != codeBuilder) codeBuilder.clear();
    if (null == dialect)
      codeBuilder =
          new CodeBuilder(
              templateResource.asTemplateContent(), name(), tagName(), this, engine, null);
    else
      codeBuilder =
          dialect.createCodeBuilder(
              templateResource.asTemplateContent(), name(), tagName(), this, engine);
    codeBuilder.build();
    extendedTemplateClass = codeBuilder.getExtendedTemplateClass();
    javaSource = codeBuilder.toString();
    if (logger.isTraceEnabled()) {
      logger.trace(
          "%s ms to generate java source for template: %s",
          System.currentTimeMillis() - start, getKey());
    }
  }

  /**
   * @return true if this class has changes refreshed, otherwise this class has not been changed yet
   */
  public boolean refresh(boolean forceRefresh) {
    if (refreshing()) return false;
    if (inner) return false;
    try {
      RythmEngine e = engine();
      refreshing(true);
      if (!templateResource.isValid()) {
        // it is removed?
        isValid = false;
        engine().classes.remove(this);
        return false;
      }
      if (null == name) {
        // this is the root level template class
        root = this;
        name = templateResource.getSuggestedClassName() + CN_SUFFIX;
        // name = templateResource.getSuggestedClassName();
        if (e.reloadByIncClassVersion()) version = nextVersion.getAndIncrement();
        engine().classes.add(this);
      }

      if (null == javaSource) {
        engine().classCache.loadTemplateClass(this);
        if (null != javaSource) {
          // try refresh extended template class if there is
          Pattern p =
              Pattern.compile(
                  ".*extends\\s+([a-zA-Z0-9_]+)\\s*\\{\\s*\\/\\/<extended_resource_key\\>(.*)\\<\\/extended_resource_key\\>.*",
                  Pattern.DOTALL);
          Matcher m = p.matcher(javaSource);
          if (m.matches()) {
            String extended = m.group(1);
            TemplateClassManager tcm = engine().classes;
            extendedTemplateClass = tcm.getByClassName(extended);
            if (null == extendedTemplateClass) {
              String extendedResourceKey = m.group(2);
              extendedTemplateClass = tcm.getByTemplate(extendedResourceKey);
              if (null == extendedTemplateClass) {
                extendedTemplateClass = new TemplateClass(extendedResourceKey, engine());
                extendedTemplateClass.refresh();
              }
            }
            engine().addExtendRelationship(extendedTemplateClass, this);
          }
        }
      }

      boolean extendedTemplateChanged = false;
      if (extendedTemplateClass != null)
        extendedTemplateChanged = extendedTemplateClass.refresh(forceRefresh);
      boolean includedTemplateChanged = false;
      if (includedTemplateClasses.size() == 0
          && !S.isEmpty(includeTemplateClassNames)
          && !NO_INCLUDE_CLASS.equals(includeTemplateClassNames)) {
        // just loaded from persistent store
        for (String tcName : includeTemplateClassNames.split(",")) {
          if (S.isEmpty(tcName)) continue;
          tcName = tcName.trim();
          String fullName = engine().testTag(tcName, this);
          if (null == fullName) {
            logger.warn("Unable to load included template class from name: %s", tcName);
            continue;
          }
          TemplateClass tc = engine().getTemplateClassFromTagName(fullName);
          if (null == tc) {
            logger.warn("Unable to load included template class from name: %s", tcName);
            continue;
          }
          includedTemplateClasses.add(tc);
        }
      }
      for (TemplateClass tc : includedTemplateClasses) {
        if (tc.refresh(forceRefresh)) {
          includedTemplateChanged = true;
          break;
        }
      }

      if (extendedTemplateChanged && engine().reloadByRestart() && !forceRefresh) {
        reset();
        compiled = false;
        engine().restart(new ClassReloadException("extended class changed"));
        refreshing(false);
        refresh(forceRefresh);
        return true; // pass refresh state to sub template
      }
      // templateResource.refresh() must be put at first so we make sure resource get refreshed

      boolean resourceChanged = templateResource.refresh();
      boolean refresh =
          resourceChanged
              || forceRefresh
              || (null == javaSource)
              || includedTemplateChanged
              || extendedTemplateChanged;
      if (!refresh) return false;

      // now start generate source and compile source to byte code
      reset();
      buildSourceCode();
      engine().classCache.cacheTemplateClassSource(this); // cache source code for debugging purpose
      if (!codeBuilder.isRythmTemplate()) {
        isValid = false;
        engine().classes.remove(this);
        return false;
      }
      isValid = true;
      // if (!engine().isProd  Mode()) logger.info(javaSource);
      compiled = false;
      return true;
    } finally {
      refreshing(false);
    }
  }

  public static final String CN_SUFFIX = "__R_T_C__";

  /**
   * Is this class already compiled but not defined ?
   *
   * @return if the class is compiled but not defined
   */
  public boolean isDefinable() {
    return compiled && javaClass != null;
  }

  /** Remove all java source/ byte code and cache */
  public void reset() {
    javaByteCode = null;
    enhancedByteCode = null;
    javaSource = null;
    templateInstance = null;
    for (TemplateClass tc : embeddedClasses) {
      tc.reset();
      engine().classes.remove(tc);
    }
    embeddedClasses.clear();
    engine().classCache.deleteCache(this);
    engine().invalidate(this);
    if (engine().reloadByIncClassVersion()) javaClass = null;
  }

  /**
   * Compile the class from Java source
   *
   * @return the bytes that comprise the class file
   */
  public byte[] compile() {
    if (null != javaByteCode) return javaByteCode;
    if (null == javaSource)
      throw new IllegalStateException("Cannot find java source when compiling " + getKey());
    compiling = true;
    long start = System.currentTimeMillis();
    try {
      engine().classes.compiler.compile(new String[] {name()});
      if (logger.isTraceEnabled()) {
        logger.trace("%sms to compile template: %s", System.currentTimeMillis() - start, getKey());
      }
    } catch (CompileException.CompilerException e) {
      String cn = e.className;
      TemplateClass tc = S.isEqual(cn, name()) ? this : engine().classes.getByClassName(cn);
      if (null == tc) tc = this;
      CompileException ce =
          new CompileException(
              tc,
              e.javaLineNumber,
              e.message); // init ce before reset java source to get template line info
      if (engine().isProdMode()) {
        TextBuilder tb = new TextBuilder();
        String[] lines = javaSource.split("(\\n\\r|\\r\\n|\\r|\\n)");
        for (int line = 0; line < lines.length; ++line) {
          tb.p(line + 1).p(":").p(lines[line]).p("\n");
        }
        logger.error("error compiling java source:\n%s", tb.toString());
      }
      javaSource =
          null; // force parser to regenerate source. This helps to reload after fixing the tag file
                // compilation failure
      throw ce;
    } catch (NullPointerException e) {
      String clazzName = name();
      TemplateClass tc = engine().classes.getByClassName(clazzName);
      if (this != tc) {
        logger.error("tc is not this");
      }
      if (!this.equals(tc)) {
        logger.error("tc not match this");
      }
      logger.error("NPE encountered when compiling template class:" + name());
      throw e;
    } finally {
      compiling = false;
    }

    if (logger.isTraceEnabled()) {
      logger.trace(
          "%sms to compile template class %s", System.currentTimeMillis() - start, getKey());
    }

    return javaByteCode;
  }

  private boolean enhancing = false;
  private transient List<TemplateClass> embeddedClasses = new ArrayList<TemplateClass>();
  /**
   * Used to instruct embedded class byte code needs to be enhanced, but for now let's just use the
   * java byte code as the enhanced bytecode
   */
  public void delayedEnhance(TemplateClass root) {
    enhancedByteCode = javaByteCode;
    root.embeddedClasses.add(this);
  }

  public byte[] enhance() {
    if (enhancing) throw new IllegalStateException("reenter enhance() call");
    enhancing = true;
    try {
      byte[] bytes = enhancedByteCode;
      if (null == bytes) {
        bytes = javaByteCode;
        if (null == bytes) bytes = compile();
        long start = System.currentTimeMillis();
        for (ITemplateClassEnhancer en : engine().templateClassEnhancers) {
          try {
            bytes = en.enhance(name(), bytes);
          } catch (Exception e) {
            logger.warn(e, "Error enhancing template class: %s", getKey());
          }
        }
        if (logger.isTraceEnabled()) {
          logger.trace(
              "%sms to enhance template class %s", System.currentTimeMillis() - start, getKey());
        }
        enhancedByteCode = bytes;
        engine().classCache.cacheTemplateClass(this);
      }
      for (TemplateClass embedded : embeddedClasses) {
        embedded.enhancedByteCode = null;
        embedded.enhance();
      }
      return bytes;
    } finally {
      enhancing = false;
    }
  }

  /** Unload the class */
  public void uncompile() {
    javaClass = null;
  }

  public boolean isClass() {
    return !name().endsWith("package-info");
  }

  public String getPackage() {
    int dot = name().lastIndexOf('.');
    return dot > -1 ? name().substring(0, dot) : "";
  }

  public void loadCachedByteCode(byte[] code) {
    enhancedByteCode = code;
  }

  //    public void compiled(byte[] code, boolean noCache) {
  //        javaByteCode = code;
  //        //enhancedByteCode = code;
  //        compiled = true;
  //        enhance();
  //        if (!noCache) engine().classCache.cacheTemplateClass(this);
  //    }

  /**
   * Call back when a class is compiled.
   *
   * @param code The bytecode.
   */
  public void compiled(byte[] code) {
    javaByteCode = code;
    // enhancedByteCode = code;
    compiled = true;
    enhance();
    // compiled(code, false);
  }

  @Override
  public String toString() {
    return "(compiled:" + compiled + ") " + name();
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) return true;
    if (o instanceof TemplateClass) {
      TemplateClass that = (TemplateClass) o;
      return that.getKey().equals(getKey());
    }
    return false;
  }

  @Override
  public int hashCode() {
    return getKey().hashCode();
  }
}
Example #4
0
public class TemplateParser implements IContext {
  private final ILogger logger = Logger.get(TemplateParser.class);
  private final CodeBuilder cb;
  private String template;
  private int totalLines;
  int cursor = 0;

  public TemplateParser(CodeBuilder cb) {
    this.template = cb.template();
    totalLines = StringUtils.countMatches(template, "\n") + 1;
    this.cb = cb;
  }

  public static class ExitInstruction extends FastRuntimeException {}

  public static class NotSIMTemplate extends FastRuntimeException {}

  void parse() {
    DialectManager dm = cb.engine.getDialectManager();
    dm.beginParse(this);
    try {
      TemplateTokenizer tt = new TemplateTokenizer(template, this);
      for (TextBuilder builder : tt) {
        cb.addBuilder(builder);
      }
    } catch (ExitInstruction e) {
      // ignore, just break the parsing process
    } catch (NotSIMTemplate e) {
      logger.error("NotSIMTemplate captured");
      cb.rewind();
      cursor = 0;
      dm.pushDef();
      try {
        TemplateTokenizer tt = new TemplateTokenizer(template, this);
        for (TextBuilder builder : tt) {
          cb.addBuilder(builder);
        }
      } catch (ExitInstruction e0) {
        // ignore just break
      } finally {
        dm.pop();
      }
    } finally {
      dm.endParse(this);
    }
  }

  @Override
  public TemplateClass getTemplateClass() {
    return cb.getTemplateClass();
  }

  @Override
  public CodeBuilder getCodeBuilder() {
    return cb;
  }

  public IDialect getDialect() {
    return cb.engine.getDialectManager().get();
  }

  public void setDialect(String dialect) {
    throw new UnsupportedOperationException();
  }

  @Override
  public String getRemain() {
    return cursor < template.length() ? template.substring(cursor) : "";
  }

  @Override
  public int cursor() {
    return cursor;
  }

  @Override
  public boolean hasRemain() {
    return cursor < template.length();
  }

  @Override
  public char peek() {
    if (!hasRemain()) return '\u0000';
    return template.charAt(cursor);
  }

  @Override
  public char pop() {
    if (!hasRemain()) throw new ArrayIndexOutOfBoundsException();
    char c = template.charAt(cursor);
    step(1);
    return c;
  }

  @Override
  public void step(int i) {
    cursor += i;
  }

  @Override
  public String getTemplateSource(int start, int end) {
    return template.substring(start, end);
  }

  private Stack<IBlockHandler> blocks = new Stack<IBlockHandler>();

  @Override
  public void openBlock(IBlockHandler bh) {
    bh.openBlock();
    blocks.push(bh);
  }

  @Override
  public IBlockHandler currentBlock() {
    return blocks.isEmpty() ? null : blocks.peek();
  }

  @Override
  public String closeBlock() throws ParseException {
    if (blocks.isEmpty())
      throw new ParseException(cb.getTemplateClass(), currentLine(), "No open block found");
    IBlockHandler bh = blocks.pop();
    return null == bh ? "" : bh.closeBlock();
  }

  @Override
  public int currentLine() {
    if (null == template) return -1; // for testing purpose only
    if (cursor >= template.length()) return totalLines;
    // return template.substring(0, cursor).split("(\\r\\n|\\n|\\r)").length;
    return StringUtils.countMatches(template.substring(0, cursor), "\n") + 1;
  }

  @Override
  public RythmEngine getEngine() {
    return cb.engine;
  }

  @Override
  public boolean compactMode() {
    if (!compactStack.empty()) return compactStack.peek();
    return getEngine().compactMode();
  }

  private Stack<Boolean> compactStack = new Stack<Boolean>();

  @Override
  public void pushCompact(Boolean compact) {
    compactStack.push(compact);
  }

  @Override
  public Boolean peekCompact() {
    if (compactStack.empty()) return null;
    return compactStack.peek();
  }

  @Override
  public Boolean popCompact() {
    if (compactStack.empty()) return null;
    return compactStack.pop();
  }

  private Stack<Break> breakStack = new Stack<Break>();

  @Override
  public void pushBreak(Break b) {
    breakStack.push(b);
  }

  @Override
  public Break peekBreak() {
    if (breakStack.empty()) return null;
    return breakStack.peek();
  }

  @Override
  public Break popBreak() {
    if (breakStack.empty()) return null;
    return breakStack.pop();
  }

  private Stack<Continue> continueStack = new Stack<Continue>();

  @Override
  public void pushContinue(Continue b) {
    continueStack.push(b);
  }

  @Override
  public Continue peekContinue() {
    if (continueStack.empty()) return null;
    return continueStack.peek();
  }

  @Override
  public Continue popContinue() {
    if (continueStack.empty()) return null;
    return continueStack.pop();
  }

  /* this constructor is just for testing purpose */
  private TemplateParser(String s) {
    template = s;
    totalLines = template.split("(\\r\\n|\\n|\\r)").length + 1;
    cb = null;
  }

  public static void main(String[] args) {
    TemplateParser tp = new TemplateParser("\nHello \n\r\nworld!");
    System.out.println(tp.totalLines);
    System.out.println(tp.currentLine());
    tp.step(5);
    System.out.println("5 steps ahead");
    System.out.println(tp.currentLine());
    System.out.println(tp.getRemain());
    tp.step(4);
    System.out.println("4 steps ahead");
    System.out.println(tp.currentLine());
    System.out.println(tp.getRemain());
  }
}