// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: SyntaxChecker.java,v 1.2 2008/04/03 08:53:23 spyromus Exp $
//
package com.salas.bb.remixfeeds.templates;
import static com.salas.bb.remixfeeds.templates.SyntaxChecker.Block.Type.*;
import org.hsqldb.lib.StringUtil;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Checks the syntax of templates.
*/
public class SyntaxChecker
{
private static final Pattern PATTERN_OPERATION = Pattern.compile("^\\s*#\\s+(.+)\\s*$");
private static final Pattern PATTERN_IF = Pattern.compile("^if\\s+single(\\s+article)?$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_ELSE = Pattern.compile("^else$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_ENDIF = Pattern.compile("^endif$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_FOR_EACH = Pattern.compile("^for\\s+each\\s+article", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_ENDFOR = Pattern.compile("^endfor$", Pattern.CASE_INSENSITIVE);
/**
* Checks the text of the template and returns the list of errors.
*
* @param text text to check.
*
* @return list of errors.
*/
public static List<SyntaxError> validate(String text)
{
LinkedList<SyntaxError> errors = new LinkedList<SyntaxError>();
// Initialize context
Block root = new Block(null, TEXT);
Context context = new Context(root);
String[] lines = text.split("\n");
int cnt = 1;
for (String line : lines)
{
List<SyntaxError> lineErrors = validate(line, cnt, context);
if (lineErrors != null && !lineErrors.isEmpty()) errors.addAll(lineErrors);
// If further validation makes no sense
if (context.currentBlock == null) break;
}
// Verify that the context is closed
if (context.currentBlock != null && context.currentBlock != root)
{
String t = "IF";
if (context.currentBlock.type == FOR) t = "FOR";
errors.add(new SyntaxError(cnt, t + " block isn't closed"));
}
return errors;
}
/**
* Validates a line.
*
* @param line line to check.
* @param num line number.
* @param context context.
*
* @return list of errors and updated context.
*/
private static List<SyntaxError> validate(String line, int num, Context context)
{
List<SyntaxError> errors = null;
// Skip empty lines
if (StringUtil.isEmpty(line)) return errors;
if (line.matches("^\\s*#.*"))
{
// Operation
Matcher m = PATTERN_OPERATION.matcher(line);
if (m.matches())
{
String operation = m.group(1);
String error = null;
boolean stopOnErorr = true;
Block currentBlock = context.currentBlock;
Block.Type currentType = currentBlock.type;
if (PATTERN_IF.matcher(operation).matches())
{
// if
if (currentType != TEXT)
{
error = "IF cannot be included in other blocks";
} else
{
context.currentBlock = new Block(currentBlock, IF);
}
} else if (PATTERN_ELSE.matcher(operation).matches())
{
// else
if (currentType != IF)
{
error = "ELSE can be only inside IF-ENDIF block";
}
} else if (PATTERN_ENDIF.matcher(operation).matches())
{
// endif
if (currentType != IF)
{
error = "ENDIF can be only inside IF block";
} else
{
context.currentBlock = currentBlock.previous;
}
} else if (PATTERN_FOR_EACH.matcher(operation).matches())
{
// for each article
if (currentType == FOR)
{
error = "FOR cannot be included";
} else
{
context.currentBlock = new Block(currentBlock, FOR);
}
} else if (PATTERN_ENDFOR.matcher(operation).matches())
{
// endfor
if (currentType != FOR)
{
error = "ENDFOR must be closing FOR";
} else
{
context.currentBlock = currentBlock.previous;
}
} else
{
// unknown operation
error = "Unknown operation: '" + operation + "'";
stopOnErorr = false;
}
if (error != null)
{
errors = new LinkedList<SyntaxError>();
errors.add(new SyntaxError(num, error));
if (stopOnErorr) context.currentBlock = null;
}
}
} else
{
// Normal line -- do nothing for now
}
return errors;
}
/**
* Context.
*/
private static class Context
{
private Block currentBlock;
private Context(Block currentBlock)
{
this.currentBlock = currentBlock;
}
}
/**
* Helper class for the hierarchy checking.
*/
static class Block
{
enum Type { TEXT, IF, FOR };
private Block previous;
private Type type;
private Block(Block previous, Type type)
{
this.previous = previous;
this.type = type;
}
}
}