Строка проверки 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)

Это связано с рядом причин:

  1. Количество свойств для факта, введенного пользователем, является переменной, поэтому я не могу просто разбить входную строку на основе знаков () и <>.
  2. Для правил иногда последние 2 свойства являются переменными, поэтому может случиться так, что свойство "Реверсивные" находится на месте в строке, где вы обычно находите свойство Negative.
  3. Если я хочу получить аргументы из этой части входной строки: +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