Строка проверки Java для специального формата, когда формат содержит циклы
Вступление
Я работаю над проектом, в котором пользователь может вводить факты и правила в специальном формате, но у меня возникают проблемы с проверкой правильности этого формата и получением информации.
Когда программа запускается, пользователь может вводить "команды" в текстовое поле, и этот текст отправляется методу parseCommand
который определяет, что делать на основе того, что написал пользователь. Например, чтобы добавить факт или правило, вы можете использовать префикс +
. или использовать -
для удаления факта или правила и так далее.
Я создал систему, которая обрабатывает префикс, но у меня возникают проблемы с форматом фактов и правил.
Факты и правила
Факты: они определяются алфавитно-цифровым именем и содержат список свойств (каждый из которых имеет знак <>
) и значение истины. Свойства также определяются буквенно-цифровым именем и содержат 2 строки (называемые аргументами), и каждый из них имеет знак <>
. Свойства также могут быть отрицательными, помещая !
перед ним в списке. Например, пользователь может ввести следующее, чтобы добавить эти 3 факта в программу:
+father(<parent(<John>,<Jake>)>, true)
+father(<parent(<Jammie>,<Jake>)>, false)
+father(!<parent(<Jammie>,<Jake>)>, true)
+familyTree(<parent(<John>,<Jake>)>, <parent(<Jammie>,<Jake>)> , true)
+fathers(<parent(<John>,<Jake>)>, !<parent(<Jammie>,<Jake>)> , true)
Класс, который я использую для хранения фактов, выглядит следующим образом:
public class Fact implements Serializable{
private boolean truth;
private ArrayList<Property> properties;
private String name;
public Fact(boolean truth, ArrayList<Property> properties, String name){
this.truth = truth;
this.properties = properties;
this.name = name;
}
//getters and setters here...
}
Правила: это ссылки между 2 свойствами, и они идентифицируются знаком =>
Снова их имя является буквенно-цифровым. Свойства ограничены, поскольку они могут иметь только аргументы, состоящие из прописных букв, а аргументы второго свойства должны быть такими же, как и первые. В правилах также есть два других аргумента, которые либо заданы, либо не установлены, введя имя или нет (каждый из этих аргументов соответствует свойству правила, которое может быть Negative
или Reversive
). например:
+son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>)
+son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>, Negative, Reversive)
+son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>, Reversive)
+son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>, Negative)
Свойства правила
Нормальное правило говорит нам, что если в приведенном ниже примере X
является родительским элементом Y
это означает, что Y
является дочерним элементом X
:
son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>)
В то время как Negative
правило говорит нам, что если в приведенном ниже примере X
является родительским элементом Y
это означает, что Y
не является дочерним элементом X
:
son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>, Negtive)
Однако правило Reversive
говорит, что если в приведенном ниже примере Y
является дочерним элементом X
это означает, что X
является родителем Y
son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>, Reversive)
Последний случай - когда правило является как Negative
и Reversive
. Это говорит нам, что если в приведенном ниже примере Y
не является дочерним элементом X
это означает, что X
является родительским элементом Y
son(<parent(<X>,<Y>)> => <child(<Y>,<X>)>, Negative, Reversive)
Это класс, который я использую для хранения правил:
public class Rule implements Serializable{
private Property derivative;
private Property impliant;
private boolean negative;
private boolean reversive;
private String name;
public Rule(Property derivative, Property impliant, boolean negative, boolean reversive) throws InvalidPropertyException{
if(!this.validRuleProperty(derivative) || !this.validRuleProperty(impliant))
throw new InvalidPropertyException("One or more properties are invalid");
this.derivative = derivative;
this.impliant = impliant;
this.negative = negative;
this.reversive = reversive;
}
//getters and setters here
}
Класс недвижимости:
public class Property implements Serializable{
private String name;
private String firstArgument;
private String secondArgument;
public Property(String name, String firstArgument, String secondArgument){
this.name = name;
this.firstArgument = firstArgument;
this.secondArgument = secondArgument;
}
Вышеприведенные примеры - все допустимые входные данные. Для пояснения здесь приведены некоторые недопустимые примеры ввода:
Факты:
Для аргумента не указано true или false:
+father(<parent(<John>,<Jake>)>)
Нет данных:
+father(false)
Предоставляется недопустимое свойство:
+father(<parent(<John>)>, true)
+father(<parent(John, Jake)>, true)
+father(<parent(John, Jake, Michel)>, true)
+father(parent(<John>,<Jake>), true)
Обратите внимание на отсутствующую скобку в последней.
Правила:
Недействительны одно или несколько свойств:
+son(<parent(<X>,<Y>)> => child(<Y>,<X>))
+son(parent(<X>,<Y>) => child(<Y>,<X>))
+son(<parent(<X>,<Y>)> => <child(<Z>,<X>)>) (Note the Z in the child property)
+son(<parent(<Not Valid>,<Y>)> => child(<Y>,<X>)) (Invalid argument for first property)
+son(=> child(<Y>,<X>))
Эта проблема
Я могу получить данные от пользователя, и я также могу видеть, какие действия пользователь хочет преформировать на основе префикса.
Однако я не могу понять, как обрабатывать строки, такие как:
+familyTree(<parent(<John>,<Jake>)>, <parent(<Jammie>,<Jake>)> , true)
Это связано с рядом причин:
- Количество свойств для факта, введенного пользователем, является переменной, поэтому я не могу просто разбить входную строку на основе знаков
()
и <>
. - Для правил иногда последние 2 свойства являются переменными, поэтому может случиться так, что свойство "Реверсивные" находится на месте в строке, где вы обычно находите свойство
Negative
. - Если я хочу получить аргументы из этой части входной строки:
+familyTree(<parent(<John>,<Jake>)>,
чтобы установить свойство для этого факта, я могу проверить все, что находится между <>
что может возникнуть проблема, потому что есть 2 открытия <
до первого >
Что я пробовал
Моя первая идея заключалась в том, чтобы начать с начала строки (что я сделал для получения действия из префикса), а затем удалить эту часть строки из основной строки.
Однако я не знаю, как адаптировать эту систему к проблемам выше (особенно проблема № 1 и 2).
Я пытался использовать такие функции, как: String.split()
и String.contains()
.
Как мне это сделать? Как я могу понять, что не все строки содержат одну и ту же информацию? (В некотором смысле, что некоторые факты имеют больше свойств или некоторые правила имеют больше атрибутов, чем другие).
РЕДАКТИРОВАТЬ:
Я забыл сказать, что все методы, используемые для хранения данных, закончены и работают, и их можно использовать, например, путем вызова: infoHandler.addRule()
или infoHandler.removeFact()
. Внутри этих функций я мог бы также проверить входные данные, если это лучше.
Я мог бы, например, просто получить все данные факта или правила из строки и проверить такие вещи, как аргументы свойств правил, только с использованием прописных букв и т.д.
EDIT 2:
В комментариях кто-то предложил использовать генератор парсера, такой как ANTLR или JavaCC. Я рассмотрел этот вариант за последние 3 дня, но я не могу найти хороший источник того, как определить в нем пользовательский язык. В большинстве документов предполагается, что вы пытаетесь скомпилировать захватывающий язык и рекомендуем загружать языковой файл откуда-то, а не писать свои собственные.
Я пытаюсь понять основы ANTLR (который, кажется, самый простой в использовании.) Однако в Интернете не так много ресурсов, чтобы помочь мне.
Если это жизнеспособный вариант, может ли кто-нибудь помочь мне понять, как сделать что-то подобное в ANTLR?
Кроме того, как только я написал файл с грамматикой, как я могу его использовать? Я читал что-то о создании парсера из языкового файла, но я не могу понять, как это делается...
ИЗМЕНИТЬ 3:
Я начал работать над грамматическим файлом для ANTLR, который выглядит так:
/** Grammer used by communicate parser */
grammar communicate;
/*
* Parser Rules
*/
argument : '<' + NAMESTRING + '>' ;
ruleArgument : '<' + RULESTRING + '>' ;
property : NAMESTRING + '(' + argument + ',' + argument + ')' ;
propertyArgument : (NEGATIVITY | POSITIVITY) + property + '>' ;
propertyList : (propertyArgument + ',')+ ;
fact : NAMESTRING + '(' + propertyList + ':' + (TRUE | FALSE) + ')';
rule : NAMESTRING + '(' + ruleArgument + '=>' + ruleArgument + ':' + RULEOPTIONS + ')' ;
/*
* Lexer Rules
*/
fragment LOWERCASE : [a-z] ;
fragment UPPERCASE : [A-Z] ;
NAMESTRING : (LOWERCASE | UPPERCASE)+ ;
RULESTRING : (UPPERCASE)+ ;
TRUE : 'True';
FALSE : 'False';
POSITIVITY : '!<';
NEGATIVITY : '<' ;
NEWLINE : ('\r'? '\n' | '\r')+ ;
RULEOPTIONS : ('Negative' | 'Negative' + ',' + 'Reversive' | 'Reversive' );
WHITESPACE : ' ' -> skip ;
Я здесь, на правильном пути? Если это хороший файл грамматики, как я могу его проверить и использовать позже?
Ответы
Ответ 1
Я не думаю, что синтаксический анализатор хорош для вашей проблемы. в любом случае вы можете справиться с этим проще с помощью регулярных выражений и некоторых утилит строк.
Лучше начать с маленькой проблемы и перейти к более крупным: сначала синтаксический анализ самого свойства кажется простым, поэтому мы пишем метод для этого:
private static Property toProp(String propStr) {
String name = propStr.substring(1,propStr.indexOf("("));
String[] arguments = propStr.substring(propStr.indexOf('(')+1,propStr.indexOf(')')).split(",");
return new Property(name,
arguments[0].substring(1,arguments[0].length()-1),
arguments[1].substring(1,arguments[1].length()-1));
}
Чтобы проанализировать строку Fact, использование regex упрощает задачу, regex для свойства is /<[\ w\d] ([<>\w\d,])>/ и с помощью метода toProp, который мы уже создали, мы можем создать другой метод для анализа фактов:
public static Fact handleFact(String factStr) {
Pattern propertyPattern = Pattern.compile("<[\\w\\d]*\\([<>\\w\\d,]*\\)>");
int s = factStr.indexOf("(") + 1;
int l = factStr.lastIndexOf(")");
String name = factStr.substring(0,s-1);
String params = factStr.substring(s, l);
Matcher matcher = propertyPattern.matcher(params);
List<Property> props = new ArrayList<>();
while(matcher.find()){
String propStr = matcher.group();
props.add(toProp(propStr));
}
String[] split = propertyPattern.split(params);
boolean truth = Boolean.valueOf(split[split.length-1].replaceAll(",","").trim());
return new Fact(truth,props,name);
}
Правила анализа очень похожи на факты:
private static Rule handleRule(String ruleStr) {
Pattern propertyPattern = Pattern.compile("<[\\w\\d]*\\([<>\\w\\d,]*\\)>");
String name = ruleStr.substring(0,ruleStr.indexOf('('));
String params = ruleStr.substring(ruleStr.indexOf('(') + 1, ruleStr.lastIndexOf(')'));
Matcher matcher = propertyPattern.matcher(params);
if(!matcher.find())
throw new IllegalArgumentException();
Property prop1 = toProp(matcher.group());
if(!matcher.find())
throw new IllegalArgumentException();
Property prop2 = toProp(matcher.group());
params = params.replaceAll("<[\\w\\d]*\\([<>\\w\\d,]*\\)>","").toLowerCase();
return new Rule(name,prop1,prop2,params.contains("negative"),params.contains("reversive"));
}
Ответ 2
Боюсь, я не могу разобрать точную грамматику, которую вы пытаетесь проанализировать из своего описания, но я понимаю, что вы пытаетесь создать объекты сущности из анализируемой грамматики. Следующие несколько демонстрационных файлов демонстрируют, как это сделать, используя ANTLR-4 и Maven:
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stackoverflow</groupId>
<artifactId>communicate</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven-compiler.version>3.6.1</maven-compiler.version>
<java.version>1.8</java.version>
<antlr.version>4.5.3</antlr.version>
<commons-io.version>2.5</commons-io.version>
<junit.version>4.12</junit.version>
</properties>
<build>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<targetPath>com/stackoverflow/test/communicate/resources</targetPath>
</testResource>
</testResources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>${antlr.version}</version>
<configuration>
<sourceDirectory>${basedir}/src/main/resources</sourceDirectory>
<outputDirectory>${basedir}/src/main/java/com/stackoverflow/communicate/frontend</outputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>antlr4</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>${antlr.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
SRC/главная/ресурсы/communicate.g4
grammar communicate;
@header {
package com.stackoverflow.communicate.frontend;
}
fact returns [com.stackoverflow.communicate.ir.Property value]
: property { $value = $property.value; }
;
property returns [com.stackoverflow.communicate.ir.Property value]
: STRING '(<' argument { com.stackoverflow.communicate.ir.ArgumentTerm lhs = $argument.value; } '>,<' argument '>)' { $value = new com.stackoverflow.communicate.ir.Property($STRING.text, lhs, $argument.value); }
;
argument returns [com.stackoverflow.communicate.ir.ArgumentTerm value]
: STRING { $value = new com.stackoverflow.communicate.ir.ArgumentTerm($STRING.text); }
;
STRING
: [a-zA-Z]+
;
SRC/главная /Java/COM/StackOverflow/общаться/л /ArgumentTerm.java
package com.stackoverflow.communicate.ir;
public class ArgumentTerm {
public String Value;
public ArgumentTerm(String value) {
Value=value;
}
}
SRC/главная /Java/COM/StackOverflow/общаться/л /Property.java
package com.stackoverflow.communicate.ir;
public class Property {
public String Name;
public ArgumentTerm Lhs;
public ArgumentTerm Rhs;
public Property(String name, ArgumentTerm lhs, ArgumentTerm rhs) {
Name=name;
Lhs=lhs;
Rhs=rhs;
}
}
SRC/тест/ресурсы/интерфейс/father.txt
parent(<John>,<Jane>)
SRC/тест/Java/COM/StackOverflow/тест/общаться/интерфейс/FrontendTest.java
package com.stackoverflow.test.communicate.frontend;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.antlr.v4.runtime.ANTLRFileStream;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
import com.stackoverflow.communicate.frontend.communicateLexer;
import com.stackoverflow.communicate.frontend.communicateParser;
public class FrontendTest {
private String testResource(String path) throws IOException {
File file=null;
try {
file=File.createTempFile("test", ".txt");
try(InputStream is=new BufferedInputStream(
FrontendTest.class.getResource(path).openStream());
OutputStream fos=new FileOutputStream(file);
OutputStream os=new BufferedOutputStream(fos)) {
IOUtils.copy(is, os);
}
CharStream fileStream=new ANTLRFileStream(file.getAbsolutePath());
communicateLexer lexer=new communicateLexer(fileStream);
TokenStream tokenStream=new CommonTokenStream(lexer);
communicateParser parser=new communicateParser(tokenStream);
ParseTree tree=parser.fact();
return tree.toStringTree(parser);
} finally {
FileUtils.deleteQuietly(file);
}
}
@Test
public void testArgumentTerm() throws IOException {
Assert.assertEquals(
"(fact (property parent (< (argument John) >,< (argument Jane) >)))",
testResource(
"/com/stackoverflow/test/communicate/resources/frontend/father.txt"));
}
}
Приложенный файл POM генерирует классы парсера (protocolParser) для грамматики..g4, если вы вызываете mvn antlr4: antlr4. FrontendTest - это единичный тест JUnit, который анализирует содержимое файла father.txt, который создает объект Property с именем "parent" и содержит два объекта термина аргумента John and Jane.
Здесь загружается полный проект Java Eclipse с этими файлами: https://www.file-upload.net/download-13056434/communicate.zip.html