Package org.gbcpainter.loaders.level.parsers

Source Code of org.gbcpainter.loaders.level.parsers.AssertValidGraphParser

package org.gbcpainter.loaders.level.parsers;

import net.jcip.annotations.NotThreadSafe;

import org.gbcpainter.geom.ImmutableSegment2D;
import org.gbcpainter.geom.Segment;
import org.gbcpainter.loaders.level.ParsingFailException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jgrapht.EdgeFactory;
import org.jgrapht.graph.SimpleGraph;

import java.awt.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.InvalidMarkException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.*;
import java.util.List;
import java.util.regex.Pattern;

/**
* Implementation of the {@link org.gbcpainter.loaders.level.parsers.FileLevelParser}.
* <p/>
* The algorithm asserts that the level described in the file is valid (connected, without overlapping elements, with junctions with at least 2 connections, etc)
* <p/>
* The format of the file is: <ol> <li> Position of the player in the format {@code x1, y1} that is, the coordinates (integers) in base ten separated by a comma </li> <li> Line
* break </li> <li> Definitions of pipes an monsters. They can be mixed and must be of the following formats: <ul> <li> {@code x1, y1, x2, y2, faceA, faceB} Defines a pipe. These
* are the coordinates (integers) in base ten separated by a comma of the 2 extremities followed by faceA and faceB, that is, 2 integers identifying the faces' perimeter this pipe
* belongs
* <p/>
* If the pipe is external faceB can be omitted  ({@code x1, y1, x2, y2, faceA}) </li> <li> {@code monster, x1, y1} Defines a monster. "monster" is the name of the monster. It must
* be a valid monster name. x1,y1 are the coordinates (integers) in base ten separated by a comma of the monster </li> </ul> </li> </ol>
*
* @author Lorenzo Pellegrini
*/
@NotThreadSafe
public class AssertValidGraphParser implements FileLevelParser {

  /* Start 'format' variables */
  @NonNls
  private static final String DIGITS_REGEX = "\\d+";
  @NonNls
  private static final String FACE_REGEX = DIGITS_REGEX;
  @NonNls
  private static final String X_COORDINATE_REGEX = DIGITS_REGEX;
  @NonNls
  private static final String Y_COORDINATE_REGEX = DIGITS_REGEX;
  @NonNls
  private static final String LINE_DELIMITER_REGEX = "[\\r\\n]+";
  /* Start inspection generated patterns */
  private static final Pattern LINE_DELIMITER_PATTERN = Pattern.compile( LINE_DELIMITER_REGEX );
  /* End 'format' variables */
  @NonNls
  private static final String[] CHARSET_NAMES = { "UTF-8", "UTF-16", "ISO-8859-1", "US-ASCII" };
  @NonNls
  private static final String DELIMITER = ",";
  @NonNls
  private static final String VALID_PIPE_LINE_REGEX = "^" + X_COORDINATE_REGEX + DELIMITER +
                                                      Y_COORDINATE_REGEX + DELIMITER +
                                                      X_COORDINATE_REGEX + DELIMITER +
                                                      Y_COORDINATE_REGEX + DELIMITER +
                                                      FACE_REGEX + DELIMITER +
                                                      FACE_REGEX + "$";
  private static final Pattern VALID_PIPE_LINE_PATTERN = Pattern.compile( VALID_PIPE_LINE_REGEX );
  @NonNls
  private static final String VALID_EXTERNAL_PIPE_LINE_REGEX = "^" + X_COORDINATE_REGEX + DELIMITER +
                                                               Y_COORDINATE_REGEX + DELIMITER +
                                                               X_COORDINATE_REGEX + DELIMITER +
                                                               Y_COORDINATE_REGEX + DELIMITER +
                                                               FACE_REGEX + "$";
  private static final Pattern VALID_EXTERNAL_PIPE_LINE_PATTERN = Pattern.compile( VALID_EXTERNAL_PIPE_LINE_REGEX );
  @NonNls
  private static final String VALID_MONSTER_LINE_REGEX = "^[a-zA-Z]+" + DELIMITER +
                                                         X_COORDINATE_REGEX + DELIMITER + Y_COORDINATE_REGEX + "$";
  private static final Pattern VALID_MONSTER_LINE_PATTERN = Pattern.compile( VALID_MONSTER_LINE_REGEX );
  @NonNls
  private static final String VALID_PLAYER_POSITION_REGEX = "^" + X_COORDINATE_REGEX + DELIMITER + Y_COORDINATE_REGEX + "$";
  private static final Pattern VALID_PLAYER_POSITION_PATTERN = Pattern.compile( VALID_PLAYER_POSITION_REGEX );
  private static final Pattern SPACE_PATTERN = Pattern.compile( "\\s" );
  /* End inspection generated patterns */


  /* Start constants for the format */
  /**
   * Number of tokens {@code x1, y1, x2, y2, faceA, faceB} (pipe definition line) has
   */
  private static final int PIPE_TOKENS = 6;

  /**
   * Number of tokens {@code x1, y1, x2, y2, faceA} (external pipe definition line) has
   */
  private static final int EXTERNAL_PIPE_TOKENS = 5;

  /**
   * Internal id of the external face
   */
  private static final int EXTERNAL_FACE_PIPE_ID = - 1;

  /**
   * Defines in which position of the line the X coordinate of the player point is.
   *
   * By the fact that the player position format is {@code x, y}, the index is 0
   */
  private static final int PLAYER_X_TOKEN = 0;

  /**
   * Defines in which position of the line the X coordinate of the player point is.
   *
   * By the fact that the player position format is {@code x, y}, the index is 1
   */
  private static final int PLAYER_Y_TOKEN = 1;

  /* Start charset decoder initialization */
  private static final List<CharsetDecoder> CHARSET_DECODERS;

  static {
    Set<CharsetDecoder> result = new LinkedHashSet<>( CHARSET_NAMES.length );

    Map<String, Charset> available = Charset.availableCharsets();
    for (String cName : CHARSET_NAMES) {
      Charset got = available.get( cName );
      if ( got != null ) {
        result.add( got.newDecoder() );
      }
    }
    CHARSET_DECODERS = new ArrayList<>( result );
  }
  /* End charset decoder initialization */

  /* Start loaded data */
  private final SimpleGraph<Point, Segment> lineGraph = new SimpleGraph<>( new EdgeFactory<Point, Segment>() {
    @Override
    public Segment createEdge( final Point sourceVertex, final Point targetVertex ) {
      return new ImmutableSegment2D( sourceVertex, targetVertex );
    }
  } );

  private final Map<Integer, Set<Segment>> facesMap = new HashMap<>( 0 );

  private boolean loaded = false;

  private Point playerPosition = new Point();

  private Map<String, List<Point>> monstersPosition = new HashMap<>( 0 );
  /* End loaded data */

  /**
   * Returns a list of charsets supported by the parser
   *
   * @return A names list of {@link java.nio.charset.Charset}
   */
  @NotNull
  public static List<String> getSupportedCharsets() {
    return new ArrayList<>( Arrays.asList( CHARSET_NAMES ) );
  }

  private synchronized static CharBuffer doDecode( @NotNull ByteBuffer buffer ) throws ParsingFailException {
    boolean success = false;
    CharBuffer parsingResult = null;
    Exception lastException = null;
    for (CharsetDecoder decoder : CHARSET_DECODERS) {
      try {
        parsingResult = decoder.decode( buffer );
        success = true;
        break;
      } catch ( IllegalStateException | CharacterCodingException notTheRightDecoder ) {
        lastException = notTheRightDecoder;
        try {
          buffer.reset();
        } catch ( InvalidMarkException noMark ) {
          buffer.clear();
        }
      }
    }

    if ( ! success ) {
      throw new ParsingFailException( "Can't find a valid decoder", lastException );
    }

    return parsingResult;
  }

  private static Point parsePlayerPositionLine( @NotNull @NonNls String line ) throws NumberFormatException {
    String[] values = line.split( DELIMITER );

    int parsedY;
    int parsedX = Integer.parseInt( values[PLAYER_X_TOKEN] );
    if ( parsedX < 0 ) {
      throw new NumberFormatException( "Negative number" );
    }

    parsedY = Integer.parseInt( values[PLAYER_Y_TOKEN] );
    if ( parsedY < 0 ) {
      throw new NumberFormatException( "Negative number" );
    }

    return new Point( parsedX, parsedY );
  }

  private static int[] parsePipeLine( @NotNull @NonNls String line ) throws ParsingFailException {
    int parsedInt;
    int tokenIndex = 0;
    int[] lineParsingRawData = new int[PIPE_TOKENS];

    StringTokenizer tokenizer = new StringTokenizer( line, DELIMITER );

    while ( tokenizer.hasMoreElements() ) {
      if ( tokenIndex == PIPE_TOKENS ) {
        throw new ParsingFailException( "Too many tokens" );
      }

      String token = tokenizer.nextToken();

      try {
        parsedInt = Integer.parseInt( token );
        if ( parsedInt < 0 ) {
          throw new NumberFormatException( "Negative number" );
        }
      } catch ( NumberFormatException e ) {
        throw new ParsingFailException( e );
      }


      lineParsingRawData[tokenIndex] = parsedInt;
      tokenIndex++;
    }

    if ( tokenIndex != PIPE_TOKENS ) {
      if ( tokenIndex == EXTERNAL_PIPE_TOKENS ) {
        lineParsingRawData[PIPE_TOKENS - 1] = EXTERNAL_FACE_PIPE_ID;
      } else {
        throw new ParsingFailException( "Not enough tokens: " + line );
      }

    }

    return lineParsingRawData;
  }

  /**
   * Utility method that parses a monster line
   *
   * @param line The line of file to parse
   *
   * @return A pair monster name: position
   *
   * @throws ParsingFailException If the line is not valid
   */
  private static Map.Entry<String, Point> parseMonsterLine( @NotNull @NonNls String line ) throws ParsingFailException {
    String monsterName;
    Point monsterPosition = new Point();
    StringTokenizer tokenizer = new StringTokenizer( line, DELIMITER );

    monsterName = tokenizer.nextToken();

    String token = tokenizer.nextToken();

    try {
      monsterPosition.x = Integer.parseInt( token );
      if ( monsterPosition.x < 0 ) {
        throw new NumberFormatException( "Negative number" );
      }
    } catch ( NumberFormatException e ) {
      throw new ParsingFailException( e );
    }

    token = tokenizer.nextToken();

    try {
      monsterPosition.y = Integer.parseInt( token );
      if ( monsterPosition.y < 0 ) {
        throw new NumberFormatException( "Negative number" );
      }
    } catch ( NumberFormatException e ) {
      throw new ParsingFailException( e );
    }

    return new AbstractMap.SimpleEntry<>( monsterName, monsterPosition );

  }

  @Override
  public void parseData( @NotNull final ByteBuffer buffer ) throws ParsingFailException {
    //Decode the byte buffer and convert it to string
    CharBuffer parsingResult = doDecode( buffer );
    String realData = parsingResult.toString();

    //Splits the file in lines
    List<String> lines = Arrays.asList( LINE_DELIMITER_PATTERN.split( realData ) );
    if ( lines.isEmpty() ) {
      throw new ParsingFailException( "Empty file" );
    }

    //Find player position first
    final Iterator<String> skipFirstLineTrick = lines.iterator();
    String firstLine;
    try {
      do {
        firstLine = skipFirstLineTrick.next();
        //Remove spaces
        firstLine = SPACE_PATTERN.matcher( firstLine ).replaceAll( "" );
      } while ( ! VALID_PLAYER_POSITION_PATTERN.matcher( firstLine ).matches() );

    } catch ( NoSuchElementException e ) {
      //If player position line couldn't be found...
      throw new ParsingFailException( "Can't find a valid player position", e );
    }

    try {
      final Point validPosition = parsePlayerPositionLine( firstLine );
      this.playerPosition.x = validPosition.x;
      this.playerPosition.y = validPosition.y;
    } catch ( NumberFormatException e ) {
      throw new ParsingFailException( e );
    }
    /* End of the parsing of the initial player position */

    /* Parse the other lines */
    while ( skipFirstLineTrick.hasNext() ) {
      @NonNls String nextLine = skipFirstLineTrick.next();

      nextLine = SPACE_PATTERN.matcher( nextLine ).replaceAll( "" );
      if ( nextLine.isEmpty() ) {
        continue;
      }

      if ( VALID_PIPE_LINE_PATTERN.matcher( nextLine ).matches() || VALID_EXTERNAL_PIPE_LINE_PATTERN.matcher( nextLine ).matches() ) {
        /* The line contains the definition of a Pipe */
        int[] lineParsingRawData = parsePipeLine( nextLine );
        Point first = new Point( lineParsingRawData[0], lineParsingRawData[1] );
        Point second = new Point( lineParsingRawData[2], lineParsingRawData[3] );
        int firstFace = lineParsingRawData[4];
        int secondFace = lineParsingRawData[5];

        this.lineGraph.addVertex( first );
        this.lineGraph.addVertex( second );

        Segment createdSegment = this.lineGraph.addEdge( first, second );

        Set<Segment> faceSet;
        /*
          If firstFace or secondFace are EXTERNAL_FACE_PIPE_ID,
            this pipe is an external pipe.
         */

        assert !(firstFace == EXTERNAL_FACE_PIPE_ID && secondFace == EXTERNAL_FACE_PIPE_ID);

        if ( firstFace != EXTERNAL_FACE_PIPE_ID ) {
          faceSet = this.facesMap.get( firstFace );
          if ( faceSet == null ) {
            //This face wasn't declared in the file yet
            faceSet = new HashSet<>( 1 );
            this.facesMap.put( firstFace, faceSet );
          }
          faceSet.add( createdSegment );
        }

        if ( secondFace != EXTERNAL_FACE_PIPE_ID ) {
          faceSet = this.facesMap.get( secondFace );
          if ( faceSet == null ) {
            //This face wasn't declared in the file yet
            faceSet = new HashSet<>( 1 );
            this.facesMap.put( secondFace, faceSet );
          }
          faceSet.add( createdSegment );
        }
      } else if ( VALID_MONSTER_LINE_PATTERN.matcher( nextLine ).matches() ) {
        /* This line contains data abount the initial position of a monster */
        Map.Entry<String, Point> parsed = parseMonsterLine( nextLine );
        List<Point> monsterList = this.monstersPosition.get( parsed.getKey() );

        if ( monsterList == null ) {
          /* A monster with the same name was not added yet */
          monsterList = new LinkedList<>();
          this.monstersPosition.put( parsed.getKey(), monsterList );
        }
        monsterList.add( parsed.getValue() );
      }
    }

    this.loaded = true;
  }

  @NotNull
  @Override
  public SimpleGraph<Point, Segment> getLevelMap() throws IllegalStateException {
    this.checkLoaded();
    return this.lineGraph;
  }

  @NotNull
  @Override
  public Map<Integer, Set<Segment>> getFacesMap() throws IllegalStateException {
    this.checkLoaded();
    return this.facesMap;
  }

  @NotNull
  @Override
  public Point getPlayerPosition() throws IllegalStateException {
    this.checkLoaded();
    return this.playerPosition;
  }

  @NotNull
  @Override
  public Map<String, List<Point>> getMonstersPosition() throws IllegalStateException {
    this.checkLoaded();
    return this.monstersPosition;
  }

  private void checkLoaded() throws IllegalStateException {
    if ( ! this.loaded ) {
      throw new IllegalStateException( "Level not loaded" );
    }
  }

  @Override
  public String toString() {
    return "AssertValidGraphParser{" +
           "lineGraph=" + this.lineGraph +
           ", facesMap=" + this.facesMap +
           ", loaded=" + this.loaded +
           ", playerPosition=" + this.playerPosition +
           ", monstersPosition=" + this.monstersPosition +
           '}';
  }
}
TOP

Related Classes of org.gbcpainter.loaders.level.parsers.AssertValidGraphParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.