/*******************************************************************************
gocha.org-lib-java Библеотека общего назначения
(с) Камнев Георгий Павлович 2009 GPLv2
Данная программа является свободным программным обеспечением. Вы вправе
распространять ее и/или модифицировать в соответствии с условиями версии 2
либо по вашему выбору с условиями более поздней версии
Стандартной Общественной Лицензии GNU, опубликованной Free Software Foundation.
Мы распространяем данную программу в надежде на то, что она будет вам полезной,
однако НЕ ПРЕДОСТАВЛЯЕМ НА НЕЕ НИКАКИХ ГАРАНТИЙ,
в том числе ГАРАНТИИ ТОВАРНОГО СОСТОЯНИЯ ПРИ ПРОДАЖЕ
и ПРИГОДНОСТИ ДЛЯ ИСПОЛЬЗОВАНИЯ В КОНКРЕТНЫХ ЦЕЛЯХ.
Для получения более подробной информации ознакомьтесь
со Стандартной Общественной Лицензией GNU.
Вместе с данной программой вы должны были получить экземпляр
Стандартной Общественной Лицензии GNU.
Если вы его не получили, сообщите об этом в Free Software Foundation, Inc.,
59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*******************************************************************************/
package org.gocha.text.regex;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.gocha.collection.Convertor;
import org.gocha.collection.Iterators;
import org.gocha.collection.NodesExtracter;
import org.gocha.collection.Predicate;
import org.gocha.collection.iterators.TreeWalk;
import org.gocha.collection.iterators.TreeWalkItreator;
import org.gocha.collection.iterators.TreeWalkType;
import org.gocha.files.FileUtil;
import org.gocha.text.TextUtil;
import org.gocha.xml.CustomAttributeSetter;
import org.gocha.xml.CustomChildrenParser;
import org.gocha.xml.XMLDecoder;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Регулярные выражения (РВ).
* В данной реализации основной формой описания РВ является XML.
* <h1>Формат регулярных выражений XML:</h1>
*
* РВ описываються тегами, часть тегов могут быть вложены друг в друга,
* ниже приводится таблица с описанием возможных тегов.
* <br />
* Типы данных в атрибутах:
* <ul>
* <li>
* Тип <b>string</b> - это тип соответствует строке с лбой последовательность символов
* </li>
*
* <li>
* <b>enum</b> - это одно значение из указаных в описании
* </li>
*
* <li>
* <b>int</b> - Указывает на цело число
* </li>
*
* <li>
* <b>bool</b> - Указывает на булево,
* так значение <b>истенно</b> может быть задно следующими выражениями:
* <b>true</b> , <b>on</b> , <b>1</b> , а значение <b>ложно</b> :
* <b>false</b> , <b>off</b> , <b>0</b>
* </li>
*
* </ul>
*
* <table border='0'>
* <tr style='font-weight:bold'>
* <td>Тег</td><td>Атрибут</td><td>Тип</td><td>Описание</td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td valign="top">text</td><td></td><td valign="top">TextPattern</td>
* <td>Задает последовательность символов.<br/>Вложенные шаблоны не поддерживаются.</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr>
* <td></td><td>text</td><td>string</td>
* <td>Искомый текст</td>
* </tr>
*
* <tr>
* <td></td><td>ignoreCase</td><td>bool</td>
* <td>Игнорировать регистр (по умолчанию off)</td>
* </tr>
*
* <tr>
* <td></td><td>invert</td><td>bool</td>
* <td>Сравнивать на не совпадение (по умолчанию off)</td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td valign="top">char</td><td></td><td valign="top">CharGroupPattern</td>
* <td>Символ из символьных групп (цифра,буква...).<br/>Вложенные шаблоны не поддерживаются.</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr>
* <td></td><td valign='top'>group</td><td valign='top'>enum</td>
* <td>
* Указывает на группу:
* <ul>
* <li>any - Любой символ</li>
* <li>whitespace - Пробельный символ</li>
* <li>uppercase - Символ в верхнем регистре</li>
* <li>spacechar - Пробельный символ</li>
* <li>lowercase - Символ в нижнем регистре</li>
* <li>letter - Буква</li>
* <li>digit - Цифра</li>
* <li>return - Символ возврата каретки - 0x0A, оно же \r</li>
* <li>nextline - Символ перевода строки - 0x0D, оно же \n</li>
* <li>tab - Символ табуляции</li>
* </ul>
* </td>
* </tr>
*
* <tr>
* <td></td><td>invert</td><td>bool</td>
* <td>Сравнивать на не совпадение (по умолчанию off)</td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td valign="top">bound</td><td></td><td valign="top">TextBoundPattern</td>
* <td>Граница текста.<br/>Вложенные шаблоны не поддерживаются.</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr>
* <td></td><td valign='top'>bound</td><td valign='top'>enum</td>
* <td>
* <ul>
* <li>word - Граница слова (буква и не буква)</li>
* <li>begin - Начала текста</li>
* <li>end - Конец текста</li>
* </ul>
* </td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td>sequence</td><td></td><td>SequencePattern</td>
* <td>Последовательность шаблонов</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td>or</td><td></td><td>OrPattern</td>
* <td>Различные варианты шаблонов (один из указанных)</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td>and</td><td></td><td>AndPattern</td>
* <td>Различные варианты шаблонов (все из указанных)</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr>
* <td></td><td>selectMax</td><td>bool</td>
* <td>Возвращать максимально длинное совпадение с текстом (по умолчанию on)</td>
* </tr>
*
* <tr>
* <td></td><td>delegateChain</td><td>bool</td>
* <td>Проверять внутри шаблонов на следующий шаблон за AND (по умолчанию on)</td>
* </tr>
*
* <tr style='background-color:#C0D0FF'>
* <td>repeat</td><td></td><td>RepeatPattern</td>
* <td>Повторения шаблона</td>
* </tr>
*
* <tr>
* <td></td><td>name</td><td>string</td>
* <td>Имя шаблона</td>
* </tr>
*
* <tr>
* <td></td><td>greedliy</td><td>bool</td>
* <td>"Жадный" алгоритм (по умолчанию on)</td>
* </tr>
*
* <tr>
* <td></td><td>compromise</td><td>bool</td>
* <td>Искать с учетом последующего шаблона после repeat (по умолчанию on)</td>
* </tr>
*
* <tr>
* <td></td><td>min</td><td>int</td>
* <td>Минимальное кол-во совпадений (-1, нет ограничения, по умолчанию)</td>
* </tr>
*
* <tr>
* <td></td><td>max</td><td>int</td>
* <td>Максимальное кол-во совпадений (-1, нет ограничения, по умолчанию)</td>
* </tr>
*
* </table>
*
* <h2>Пример описания числа:</h2>
* <sequence name='num'><br />
* <repeat name='sign' min='0' max='1' greedily='on'><br />
* <text text='-' /><br />
* </repeat><br />
* <repeat name='digit' min='1' max='-1' greedily='on'><br />
* <char group='digit' /><br />
* </repeat><br />
* <repeat min='0' max='1' greedily='on'><br />
* <sequence><br />
* <text name='fpoint' text='.' /><br />
* <repeat name='digit' min='1' max='-1' greedily='on'><br />
* <char group='digit' /><br />
* </repeat><br />
* </sequence><br />
* </repeat><br />
* </sequence><br />
*
* @author gocha
*/
public class Regex
{
public Regex()
{
}
public static Pattern parseWildcard(String wildcard,boolean ignoreCase,char any,char anyRepeat,char escape)
{
if (wildcard == null) {
throw new IllegalArgumentException("wildcard == null");
}
if( wildcard.length()<1 )
{
throw new IllegalArgumentException("wildcard.length() < 1");
}
SequencePattern seq = new SequencePattern();
String buff = "";
boolean escp = false;
for( int i=0; i<wildcard.length(); i++ )
{
char c = wildcard.charAt(i);
if( !escp )
{
if( c==any )
{
if( buff.length()>0 )
{
TextPattern tm = new TextPattern(buff, ignoreCase);
seq.add(tm);
buff = "";
}
CharGroupPattern cg = new CharGroupPattern(CharGroupName.ANY);
seq.add(cg);
}else if( c==anyRepeat )
{
if( buff.length()>0 )
{
TextPattern tm = new TextPattern(buff, ignoreCase);
seq.add(tm);
buff = "";
}
CharGroupPattern cg = new CharGroupPattern(CharGroupName.ANY);
// OrPattern or = new OrPattern();
RepeatPattern rep = new RepeatPattern();
rep.setSubPattern(cg);
rep.setMin(0);
rep.setMax(-1);
rep.setGreedily(false);
rep.setCompromise(true);
seq.add(rep);
}else if( c==escape )
{
escp = true;
}else{
buff += c;
}
}else{
escp = false;
buff += c;
}
}
if( buff.length()>0 )
{
TextPattern tm = new TextPattern(buff, ignoreCase);
seq.add(tm);
buff = "";
}
TextBoundPattern endTextBound = new TextBoundPattern(TextBoundName.ENDTEXT);
seq.add(endTextBound);
return seq;
}
/**
* Анализирует XML и возвращает шаблон регулярных выражений
* @param xmlDocument Шаблон в формате XML
* @return Шаблон регулярных выражений или null, если не получилось создать
* @see Regex
*/
public static Pattern parseXML(org.w3c.dom.Document xmlDocument)
{
if (xmlDocument == null) {
throw new IllegalArgumentException("xmlDocument == null");
}
XMLDecoder decoder = new XMLDecoder();
configureXMLDecoder(decoder);
Object o = decoder.parse(xmlDocument);
return (o instanceof Pattern) ? (Pattern)o : null;
}
/**
* Анализирует XML и возвращает шаблон регулярных выражений
* @param xml Шаблон в формате XML
* @return Шаблон регулярных выражений или null, если не получилось создать
* @see Regex
*/
public static Pattern parseXML(org.w3c.dom.Node xml)
{
if (xml == null) {
throw new IllegalArgumentException("xml == null");
}
XMLDecoder decoder = new XMLDecoder();
configureXMLDecoder(decoder);
Object o = decoder.parse(xml);
return (o instanceof Pattern) ? (Pattern)o : null;
}
/**
* Анализирует XML и возвращает шаблон регулярных выражений
* @param xml Шаблон в формате XML
* @return Шаблон регулярных выражений или null, если не получилось создать
* @see Regex
*/
public static Pattern parseXML(String xml) throws IOException, SAXException
{
if (xml == null) {
throw new IllegalArgumentException("xml == null");
}
XMLDecoder decoder = new XMLDecoder();
configureXMLDecoder(decoder);
Object o = decoder.parseXML(xml);
return (o instanceof Pattern) ? (Pattern)o : null;
}
private static void configureXMLDecoder(XMLDecoder decoder)
{
decoder.putAttributeSetter(CharGroupPattern.class, "group", charGroup);
decoder.putAttributeSetter(TextBoundPattern.class, "type", boundsAttr);
decoder.putChildrenParser(RepeatPattern.class, repeatParser(decoder));
decoder.getTagMap().put("char", CharGroupPattern.class);
decoder.getTagMap().put("or", OrPattern.class);
decoder.getTagMap().put("repeat", RepeatPattern.class);
decoder.getTagMap().put("sequence", SequencePattern.class);
decoder.getTagMap().put("text", TextPattern.class);
decoder.getTagMap().put("bound", TextBoundPattern.class);
decoder.getTagMap().put("and", AndPattern.class);
}
/**
* Анализирует XML заданный в ресурсе и возвращает шаблон регулярных выражений
* @param resourceName Имя ресурса
* @return Шаблон регулярных выражений или null, если не получилось создать
* @see Regex
*/
public static Pattern parseXMLResource(String resourceName) throws IOException, SAXException
{
return parseXMLResource(resourceName, null);
}
/**
* Анализирует XML заданный в ресурсе и возвращает шаблон регулярных выражений
* @param resourceName Имя ресурса
* @param cs Кодировка ресурса
* @return Шаблон регулярных выражений или null, если не получилось создать
* @see Regex
*/
public static Pattern parseXMLResource(String resourceName, Charset cs) throws IOException, SAXException
{
if (resourceName == null) {
throw new IllegalArgumentException("resourceName == null");
}
if (cs==null) cs = FileUtil.UTF8();
URL res = Regex.class.getResource(resourceName);
if( res==null )return null;
return parseXML( FileUtil.readAllText(res, cs) );
}
private static CustomAttributeSetter boundsAttr = new CustomAttributeSetter() {
@Override
public void set(Object obj, String attribute, String value) {
if( !(obj instanceof TextBoundPattern) )return;
if( attribute==null || value==null )return;
if( attribute.equalsIgnoreCase("name") )
{
((TextBoundPattern)obj).setName(value);
}
if( attribute.equalsIgnoreCase("type") )
{
if( value.equalsIgnoreCase("word") )
{
((TextBoundPattern)obj).setTextBoundName(TextBoundName.WORDBREAK);
}else if( value.equalsIgnoreCase("begin") ){
((TextBoundPattern)obj).setTextBoundName(TextBoundName.BEGINTEXT);
}else if(
value.equalsIgnoreCase("end") ||
value.equalsIgnoreCase("eof")
){
((TextBoundPattern)obj).setTextBoundName(TextBoundName.ENDTEXT);
}
}
}
};
private static CustomChildrenParser repeatParser(XMLDecoder decoder)
{
final XMLDecoder fdecoder = decoder;
return new CustomChildrenParser() {
@Override
public void parse(Object parentObject, Node parentNode) {
if( parentObject==null || !(parentObject instanceof RepeatPattern) )return;
if( parentNode==null )return;
if( !parentNode.hasChildNodes() )return;
NodeList nl = parentNode.getChildNodes();
int count = nl.getLength();
// int parsed = 0;
for( int i=0; i<count; i++ )
{
Node n = nl.item(i);
if( n==null || !(n instanceof Element) )continue;
Object obj = fdecoder.parse(n);
if( obj==null )continue;
if( obj instanceof Pattern )
{
((RepeatPattern)parentObject).setSubPattern((Pattern)obj);
break;
}
}
}
};
}
private static CustomAttributeSetter charGroup = new CustomAttributeSetter() {
@Override
public void set(Object obj, String attribute, String value) {
if( !(obj instanceof CharGroupPattern) )return;
if( attribute==null || value==null )return;
if( attribute.equalsIgnoreCase("name") )
{
((CharGroupPattern)obj).setName(value);
}
if( attribute.equalsIgnoreCase("group") )
{
if( value.equalsIgnoreCase("any") )
{
((CharGroupPattern)obj).setCharGroup(CharGroupName.ANY);
}else if( value.equalsIgnoreCase("WHITESPACE") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.WHITESPACE);
}else if( value.equalsIgnoreCase("UPPERCASE") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.UPPERCASE);
}else if( value.equalsIgnoreCase("SPACECHAR") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.SPACECHAR);
}else if( value.equalsIgnoreCase("LOWERCASE") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.LOWERCASE);
}else if( value.equalsIgnoreCase("LETTER") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.LETTER);
}else if( value.equalsIgnoreCase("DIGIT") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.DIGIT);
}else if( value.equalsIgnoreCase("RETURN") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.RETURN);
}else if( value.equalsIgnoreCase("NEXTLINE") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.NEXTLINE);
}else if( value.equalsIgnoreCase("TAB") ){
((CharGroupPattern)obj).setCharGroup(CharGroupName.TAB);
}
}
}
};
public final static NodesExtracter<Matcher,Matcher> matchExtracter = new NodesExtracter<Matcher, Matcher>()
{
@Override
public Iterable<Matcher> extract(Matcher from)
{
return from.getChildrenMatchers();
}
};
public final static Convertor<TreeWalk<Matcher>,Matcher> treeWalkConvertor = new Convertor<TreeWalk<Matcher>, Matcher>()
{
@Override
public Matcher convert(TreeWalk<Matcher> from)
{
return from.currentNode();
}
};
public static Iterable<TreeWalk<Matcher>> walkTree(Matcher root)
{
if (root == null) {
throw new IllegalArgumentException("root == null");
}
Iterable<TreeWalk<Matcher>> itr = TreeWalkItreator.<Matcher>createIterable(root, matchExtracter);
return itr;
}
public static Iterable<Matcher> walkMatchers(Matcher root)
{
if (root == null) {
throw new IllegalArgumentException("root == null");
}
Iterable<TreeWalk<Matcher>> itr = walkTree(root);
Iterable<Matcher> converted = Iterators.<TreeWalk<Matcher>,Matcher>convert(itr, treeWalkConvertor);
return converted;
}
public static String getMatcherName(Matcher m)
{
if( m==null )return null;
Pattern p = m.getPattern();
String n = p!=null ? p.name() : null;
return n;
}
public static Matcher getFirstNamedMatcher(Matcher root, String name)
{
if (root == null) {
throw new IllegalArgumentException("root == null");
}
if (name == null) {
throw new IllegalArgumentException("name == null");
}
for( Matcher m : walkMatchers(root) )
{
String mn = getMatcherName(m);
if( mn==null )continue;
if( mn.equals(name) )return m;
}
return null;
}
public static Iterable<Matcher> getMatchers(Matcher root, Predicate<Matcher> matcherPredicate)
{
if (root == null) {
throw new IllegalArgumentException("root == null");
}
if (matcherPredicate == null) {
throw new IllegalArgumentException("matcherPredicate == null");
}
ArrayList<Matcher> result = new ArrayList<Matcher>();
for( Matcher m : walkMatchers(root) )
{
if( m==null )continue;
if( matcherPredicate.validate(m) )
{
result.add(m);
}
}
return result;
}
public static Iterable<Matcher> getNamedMatchers(Matcher root, String name)
{
if (root == null) {
throw new IllegalArgumentException("root == null");
}
if (name == null) {
throw new IllegalArgumentException("name == null");
}
ArrayList<Matcher> result = new ArrayList<Matcher>();
for( Matcher m : walkMatchers(root) )
{
String mn = getMatcherName(m);
if( mn==null )continue;
if( mn.equals(name) )
{
result.add(m);
}
}
return result;
}
public static Map<String,List<Matcher>> getNamedMatchersMap(Matcher root)
{
if (root == null) {
throw new IllegalArgumentException("root == null");
}
Map<String,List<Matcher>> result = new TreeMap<String, List<Matcher>>();
for( Matcher m : walkMatchers(root) )
{
if( m==null )continue;
String name = getMatcherName(m);
if( name==null )continue;
List<Matcher> l = null;
if( result.containsKey(name) )
{
l = result.get(name);
}
if( l==null )
{
l = new ArrayList<Matcher>();
result.put(name, l);
}
l.add(m);
}
return result;
}
public static String getNamedMatchersString(Matcher root, String name, String delimiter)
{
if (name == null) {
throw new IllegalArgumentException("name == null");
}
StringBuilder sb = new StringBuilder();
int co = -1;
for( Matcher m : getNamedMatchers(root, name) )
{
if( m==null )continue;
if( !m.isMatched() )continue;
String mtext = m.getMatchedText();
if( mtext==null )continue;
co++;
if( co>0 && delimiter!=null )sb.append(delimiter);
sb.append(mtext);
}
return sb.toString();
}
public static Predicate<Matcher> matcherNameWildcard(String mask,boolean ignoreCase,char any,char anyRepeat,char escape)
{
if (mask == null) {
throw new IllegalArgumentException("mask == null");
}
final Pattern ptrn = parseWildcard(mask, ignoreCase, any, anyRepeat, escape);
if( ptrn==null )
throw new IllegalArgumentException(
"can't create pattern of wildcard = "+ TextUtil.encodeStringConstant(mask));
return new Predicate<Matcher>()
{
@Override
public boolean validate(Matcher value)
{
if( value==null )return false;
Pattern p = value.getPattern();
String n = p==null ? null : p.name();
if( n==null )return false;
Matcher m = ptrn.match(n, 0);
return m==null ? false : m.isMatched();
}
};
}
public static Predicate<Matcher> matcherNameWildcard(String mask,boolean ignoreCase)
{
if (mask == null) {
throw new IllegalArgumentException("mask == null");
}
return matcherNameWildcard(mask, ignoreCase, '?', '*', '\\');
}
public static Predicate<Matcher> matcherNameWildcard(String mask)
{
if (mask == null) {
throw new IllegalArgumentException("mask == null");
}
return matcherNameWildcard(mask, true, '?', '*', '\\');
}
private static NodesExtracter<Matcher,Matcher> resultView = new NodesExtracter<Matcher,Matcher>() {
@Override
public Iterable<Matcher> extract(Matcher from)
{
return from.getChildrenMatchers();
}
};
public static void print(Matcher m)
{
if (m == null) {
throw new IllegalArgumentException("m == null");
}
Iterable<TreeWalk<Matcher>> itr =
TreeWalkItreator.<Matcher>createIterable(m, resultView,TreeWalkType.ByBranchForward);
for( TreeWalk<Matcher> twm : itr )
{
print(twm);
}
}
public static void print(TreeWalk<Matcher> twm)
{
if (twm == null) {
throw new IllegalArgumentException("twm == null");
}
Matcher m = twm.currentNode();
int l = twm.currentLevel();
if( l<0 )l=0;
String spacer = "";
for( int i=0; i<l; i++ )
{
spacer += "....";
}
Matcher sm = m;
Pattern ptrn = sm.getPattern();
String name = ptrn.name();
name = name != null ?
" name="+TextUtil.encodeStringConstant(name)
: null;
Class ptrnCls = ptrn.getClass();
String sptrn = ptrnCls.getSimpleName();
if( sptrn==null )sptrn = ptrnCls.getName();
String matchedText = null;
if( m.isMatched() ){
String begin = " begin="+sm.getBegin();
String mtext = " matched="+TextUtil.encodeStringConstant(sm.getMatchedText());
matchedText = begin+mtext;
}else{
matchedText = " not matched";
}
System.out.println(spacer+sptrn+(name==null ? "" : name)+matchedText);
}
}