/** * Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.roboconf.core.internal.dsl.parsing; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.roboconf.core.ErrorCode; import net.roboconf.core.dsl.ParsingConstants; import net.roboconf.core.dsl.parsing.AbstractBlock; import net.roboconf.core.dsl.parsing.AbstractBlockHolder; import net.roboconf.core.dsl.parsing.AbstractIgnorableInstruction; import net.roboconf.core.dsl.parsing.BlockBlank; import net.roboconf.core.dsl.parsing.BlockComment; import net.roboconf.core.dsl.parsing.BlockComponent; import net.roboconf.core.dsl.parsing.BlockFacet; import net.roboconf.core.dsl.parsing.BlockImport; import net.roboconf.core.dsl.parsing.BlockInstanceOf; import net.roboconf.core.dsl.parsing.BlockProperty; import net.roboconf.core.dsl.parsing.BlockUnknown; import net.roboconf.core.dsl.parsing.FileDefinition; import net.roboconf.core.model.ParsingError; import net.roboconf.core.utils.Utils; /** * A parser for relation files. * @author Vincent Zurczak - Linagora */ public class FileDefinitionParser { static final int P_CODE_YES = 1; static final int P_CODE_NO = 2; static final int P_CODE_CANCEL = 3; private static final char O_CURLY_BRACKET = '{'; private static final char C_CURLY_BRACKET = '}'; private static final char SEMI_COLON = ';'; private final FileDefinition definitionFile; private boolean ignoreComments = true; private boolean lastLineEndedWithLineBreak = false; int currentLineNumber; /** * Constructor. * @param relationsFile the relation file (not null) * @param ignoreComments true to ignore comments during parsing */ public FileDefinitionParser( File relationsFile, boolean ignoreComments ) { this.ignoreComments = ignoreComments; this.currentLineNumber = 0; this.definitionFile = new FileDefinition( relationsFile ); } /** * Reads a definition file. * @return an instance of {@link FileDefinition} (never null) * <p> * Parsing errors are stored in the result.<br> * See {@link FileDefinition#getParsingErrors()}. * </p> */ public FileDefinition read() { // Parse blocks try { fillIn(); mergeContiguousRegions( this.definitionFile.getBlocks()); } catch( IOException e ) { addModelError( ErrorCode.P_IO_ERROR, this.currentLineNumber, e.getMessage()); } // Determine file type boolean hasFacets = false, hasComponents = false, hasInstances = false, hasImports = false; int ignorableBlocksCount = 0; for( AbstractBlock block : this.definitionFile.getBlocks()) { if( block.getInstructionType() == AbstractBlock.COMPONENT ) hasComponents = true; else if( block.getInstructionType() == AbstractBlock.FACET ) hasFacets = true; else if( block.getInstructionType() == AbstractBlock.INSTANCEOF ) hasInstances = true; else if( block.getInstructionType() == AbstractBlock.IMPORT ) hasImports = true; else if( block instanceof AbstractIgnorableInstruction ) ignorableBlocksCount ++; } if( hasInstances ) { if( ! hasFacets && ! hasComponents ) this.definitionFile.setFileType( FileDefinition.INSTANCE ); else addModelError( ErrorCode.P_INVALID_FILE_TYPE, 1 ); } else if( hasFacets || hasComponents ) { this.definitionFile.setFileType( FileDefinition.GRAPH ); } else if( hasImports ) { this.definitionFile.setFileType( FileDefinition.AGGREGATOR ); } else if( ignorableBlocksCount == this.definitionFile.getBlocks().size()) { addModelError( ErrorCode.P_EMPTY_FILE, 1 ); this.definitionFile.setFileType( FileDefinition.EMPTY ); } return this.definitionFile; } /** * @param line the raw line * @return one of the P_CODE constants from {@link FileDefinitionParser} */ int recognizeComponent( String line, BufferedReader br ) throws IOException { int result = P_CODE_NO; String alteredLine = line.trim().toLowerCase(); if( ! alteredLine.isEmpty() && ! alteredLine.startsWith( String.valueOf( ParsingConstants.COMMENT_DELIMITER )) && ! startsWith( alteredLine, ParsingConstants.KEYWORD_FACET ) && ! startsWith( alteredLine, ParsingConstants.KEYWORD_INSTANCE_OF ) && ! startsWith( alteredLine, ParsingConstants.KEYWORD_IMPORT )) result = recognizePropertiesHolder( line, br, new BlockComponent( this.definitionFile )); return result; } /** * @param line the raw line * @return one of the P_CODE constants from {@link FileDefinitionParser} */ int recognizeFacet( String line, BufferedReader br ) throws IOException { int result = P_CODE_NO; if( startsWith( line, ParsingConstants.KEYWORD_FACET )) { String newLine = line.replaceFirst( "(?i)\\s*" + Pattern.quote( ParsingConstants.KEYWORD_FACET ), "" ); result = recognizePropertiesHolder( newLine, br, new BlockFacet( this.definitionFile )); } return result; } /** * @param line the raw line * @param holderInstance * @return one of the P_CODE constants from {@link FileDefinitionParser} */ int recognizeInstanceOf( String line, BufferedReader br, AbstractBlockHolder holderInstance ) throws IOException { int result = P_CODE_NO; if( startsWith( line, ParsingConstants.KEYWORD_INSTANCE_OF )) { String newLine = line.replaceFirst( "(?i)\\s*" + Pattern.quote( ParsingConstants.KEYWORD_INSTANCE_OF ), "" ); BlockInstanceOf newInstance = new BlockInstanceOf( this.definitionFile ); result = recognizePropertiesHolder( newLine, br, newInstance ); // Handle imbricated instances if( result == P_CODE_YES && holderInstance != null ) { this.definitionFile.getBlocks().remove( newInstance ); holderInstance.getInnerBlocks().add( newInstance ); } } return result; } /** * @param line the raw line * @param blocks the blocks to update * @return {@link #P_CODE_YES} or {@link #P_CODE_NO} */ int recognizeComment( String line, Collection<AbstractBlock> blocks ) { int result = P_CODE_NO; if( line.trim().startsWith( ParsingConstants.COMMENT_DELIMITER )) { result = P_CODE_YES; if( ! this.ignoreComments ) blocks.add( new BlockComment( this.definitionFile, line )); } return result; } /** * @param line the raw line * @param blocks the blocks to update * @return {@link #P_CODE_YES} or {@link #P_CODE_NO} */ int recognizeBlankLine( String line, Collection<AbstractBlock> blocks ) { int result = P_CODE_NO; if( Utils.isEmptyOrWhitespaces( line )) { result = P_CODE_YES; blocks.add( new BlockBlank( this.definitionFile, line )); } return result; } /** * @param line the raw line * @param holder the properties holder * @return one of the P_CODE constants from {@link FileDefinitionParser} */ int recognizeProperty( String line, AbstractBlockHolder holder ) { int result = P_CODE_NO; String[] parts = splitFromInlineComment( line ); String realLine = parts[ 0 ].trim(); String regex = "([^:\\s]+)\\s*:\\s*(.*)$"; Matcher m = Pattern.compile( regex ).matcher( realLine ); if( m.find()) { // A property was identified result = P_CODE_YES; BlockProperty block = new BlockProperty( this.definitionFile ); block.setLine( this.currentLineNumber ); block.setName( m.group( 1 )); // Properties end with a semicolon if( ! m.group( 2 ).endsWith( ";" )) addModelError( ErrorCode.P_PROPERTY_ENDS_WITH_SEMI_COLON ); block.setValue( m.group( 2 ).replaceFirst( ";$", "" )); block.setInlineComment( parts[ 1 ]); holder.getInnerBlocks().add( block ); // A property block should only contain one semicolon. // Only exception: exported variables, that can contain semicolons in their quoted values. String escapedLine = block.getValue(); if( realLine.contains( "\"" )) { escapedLine = escapedLine.replaceAll( "\"[^\"]*\"", "" ); } if( escapedLine.contains( ";" )) addModelError( ErrorCode.P_ONE_BLOCK_PER_LINE ); } return result; } /** * @param line the raw line * @return one of the P_CODE constants from {@link FileDefinitionParser} */ int recognizeImport( String line ) { int result = P_CODE_NO; String[] parts = splitFromInlineComment( line ); String realLine = parts[ 0 ].trim(); String regex = ParsingConstants.KEYWORD_IMPORT + "\\s+([^;]*)"; Matcher m = Pattern.compile( regex, Pattern.CASE_INSENSITIVE ).matcher( realLine ); if( m.find()) { result = P_CODE_YES; BlockImport block = new BlockImport( this.definitionFile ); block.setLine( this.currentLineNumber ); block.setUri( m.group( 1 ).trim()); block.setInlineComment( parts[ 1 ]); this.definitionFile.getBlocks().add( block ); realLine = realLine.substring( m.end()); if( ! realLine.startsWith( String.valueOf( SEMI_COLON ))) addModelError( ErrorCode.P_IMPORT_ENDS_WITH_SEMI_COLON ); else if( realLine.indexOf( SEMI_COLON ) < realLine.length() - 1 ) addModelError( ErrorCode.P_ONE_BLOCK_PER_LINE ); } return result; } /** * Splits the line from the comment delimiter. * @param line a string (not null) * @return an array of 2 strings * <p> * Index 0: the line without the in-line comment. Never null.<br> * Index 1: the in-line comment (if not null, it starts with a '#' symbol). * </p> */ String[] splitFromInlineComment( String line ) { String[] result = new String[] { line, "" }; int index = line.indexOf( ParsingConstants.COMMENT_DELIMITER ); if( index >= 0 ) { result[ 0 ] = line.substring( 0, index ); if( ! this.ignoreComments ) { // Find extra spaces before the in-line comment and put them in the comment Matcher m = Pattern.compile( "(\\s+)$" ).matcher( result[ 0 ]); String prefix = ""; if( m.find()) prefix = m.group( 1 ); result[ 1 ] = prefix + line.substring( index ); } result[ 0 ] = result[ 0 ].trim(); } return result; } /** * Merges the contiguous regions for which it makes sense. * <p> * Contiguous comments are merged together, as well as contiguous blank regions. * This reduces the number of regions. * </p> * * @param blocks */ void mergeContiguousRegions( Collection<AbstractBlock> blocks ) { AbstractIgnorableInstruction initialInstr = null; List<AbstractBlock> toRemove = new ArrayList<> (); StringBuilder sb = new StringBuilder(); // We only merge comments and blank regions to reduce their number for( AbstractBlock block : blocks ) { if( initialInstr == null ) { if( block.getInstructionType() == AbstractBlock.COMMENT || block.getInstructionType() == AbstractBlock.BLANK ) { AbstractIgnorableInstruction currentInstr = (AbstractIgnorableInstruction) block; initialInstr = currentInstr; sb = new StringBuilder( currentInstr.getContent()); } } else if( initialInstr.getInstructionType() == block.getInstructionType()) { toRemove.add( block ); sb.append( System.getProperty( "line.separator" )); sb.append(((AbstractIgnorableInstruction) block).getContent()); } else { initialInstr.setContent( sb.toString()); initialInstr = null; } } // Remove the blocks that have been merged blocks.removeAll( toRemove ); // In a second time, we can reduce facets and components too for( AbstractBlock block : blocks ) { if( block.getInstructionType() == AbstractBlock.COMPONENT || block.getInstructionType() == AbstractBlock.FACET ) mergeContiguousRegions(((AbstractBlockHolder) block).getInnerBlocks()); } } /** * @return the definitionFile (for tests) */ FileDefinition getFileRelations() { return this.definitionFile; } /** * @param line * @param br * @param holderInstance * @return an integer code * <p> * {@value P_CODE_NO} if not recognized, * {@value P_CODE_YES} if it is and {@value P_CODE_CANCEL} otherwise. * </p> * * @throws IOException */ private int recognizePropertiesHolder( String line, BufferedReader br, AbstractBlockHolder holderInstance ) throws IOException { int result = P_CODE_NO; String[] parts = splitFromInlineComment( line ); String realLine = parts[ 0 ].trim(); // Recognize the declaration AbstractBlockHolder holder = null; StringBuilder sb = new StringBuilder(); boolean endInstructionReached = false, foundExtraChars = false; for( char c : realLine.toCharArray()) { if( c == O_CURLY_BRACKET ) endInstructionReached = true; else if( ! endInstructionReached ) sb.append( c ); else { foundExtraChars = true; break; } } if( foundExtraChars ) { addModelError( ErrorCode.P_O_C_BRACKET_EXTRA_CHARACTERS ); result = P_CODE_CANCEL; } else if( ! endInstructionReached ) { if( Utils.isEmptyOrWhitespaces( sb.toString()) || sb.toString().matches( ParsingConstants.PATTERN_ID )) { addModelError( ErrorCode.P_O_C_BRACKET_MISSING ); result = P_CODE_CANCEL; } else { result = P_CODE_NO; } } else { result = P_CODE_YES; holder = holderInstance; holder.setName( sb.toString().trim()); holder.setLine( this.currentLineNumber ); holder.setInlineComment( parts[ 1 ]); this.definitionFile.getBlocks().add( holder ); } // Recognize the properties boolean errorInSubProperties = false; if( holder != null ) { while(( line = nextLine( br )) != null && ! line.trim().startsWith( String.valueOf( C_CURLY_BRACKET ))) { int code = recognizeBlankLine( line, holder.getInnerBlocks()); if( code == P_CODE_YES ) continue; code = recognizeComment( line, holder.getInnerBlocks()); if( code == P_CODE_YES ) continue; code = recognizeInstanceOf( line, br, holderInstance ); if( code == P_CODE_NO ) code = recognizeProperty( line, holder ); if( code == P_CODE_CANCEL ) result = P_CODE_CANCEL; if( code != P_CODE_YES ) { errorInSubProperties = true; break; } } } // Why did we exit the loop? // 1. We found an invalid content for the holder. // 2. We reached EOF or we found a closing curly bracket. // 3. We never entered the loop! // Inner errors prevail if( errorInSubProperties ) { if( result == P_CODE_YES ) { if( holderInstance.getInstructionType() == AbstractBlock.INSTANCEOF ) addModelError( ErrorCode.P_INVALID_PROPERTY_OR_INSTANCE ); else addModelError( ErrorCode.P_INVALID_PROPERTY ); } result = P_CODE_CANCEL; } // Inner blocks are valid, we found a curly bracket, check the end else if( result == P_CODE_YES && line != null && line.trim().startsWith( String.valueOf( C_CURLY_BRACKET ))) { line = line.replaceFirst( "\\s*\\}", "" ); parts = splitFromInlineComment( line ); if( ! Utils.isEmptyOrWhitespaces( parts[ 0 ])) { addModelError( ErrorCode.P_C_C_BRACKET_EXTRA_CHARACTERS ); result = P_CODE_CANCEL; } holder.setClosingInlineComment( parts[ 1 ]); } // The closing bracket is missing else if( result == P_CODE_YES ) { addModelError( ErrorCode.P_C_C_BRACKET_MISSING ); } return result; } /** * @param br * @return the next read line, or null if the buffer's end was reached * @throws IOException */ String nextLine( BufferedReader br ) throws IOException { // {@link BufferedReader#readLine()} does not allow to detect when the last line is empty. // We need this precision. So, we read character by character. int c = 0; StringBuilder sb = new StringBuilder(); while(( c = br.read()) != -1 && ((char) c) != '\n' ) { if((char) c != '\r' ) sb.append((char) c); } if( sb.length() > 0 ) this.lastLineEndedWithLineBreak = c != -1; String line = c == -1 && sb.length() == 0 ? null : sb.toString(); this.currentLineNumber ++; return line; } /** * Parses the file and fills-in the resulting structure. * @throws IOException */ private void fillIn() throws IOException { BufferedReader br = null; InputStream in = null; try { in = new FileInputStream( this.definitionFile.getEditedFile()); br = new BufferedReader( new InputStreamReader( in, "UTF-8" )); String line; while(( line = nextLine( br )) != null ) { int code = recognizeBlankLine( line, this.definitionFile.getBlocks()); if( code == P_CODE_YES ) continue; code = recognizeComment( line, this.definitionFile.getBlocks()); if( code == P_CODE_YES ) continue; code = recognizeImport( line ); if( code == P_CODE_CANCEL ) break; else if( code == P_CODE_YES ) continue; code = recognizeFacet( line, br ); if( code == P_CODE_CANCEL ) break; else if( code == P_CODE_YES ) continue; code = recognizeInstanceOf( line, br, null ); if( code == P_CODE_CANCEL ) break; else if( code == P_CODE_YES ) continue; // "recognizeComponent" is the last attempt to identify the line. code = recognizeComponent( line, br ); // So, eventually, we add the line as an unknown block. if( code != P_CODE_YES ) this.definitionFile.getBlocks().add( new BlockUnknown( this.definitionFile, line )); // Deal with the error codes. // Cancel: break the loop. // No: try to process the next line. if( code == P_CODE_CANCEL ) break; if( code == P_CODE_NO ) addModelError( ErrorCode.P_UNRECOGNIZED_BLOCK ); } if( line == null && this.lastLineEndedWithLineBreak ) this.definitionFile.getBlocks().add( new BlockBlank( this.definitionFile, "" )); } finally { Utils.closeQuietly( br ); Utils.closeQuietly( in ); } } /** * @param errorCode */ private void addModelError( ErrorCode errorCode ) { ParsingError me = new ParsingError( errorCode, this.definitionFile.getEditedFile(), this.currentLineNumber ); this.definitionFile.getParsingErrors().add( me ); } /** * @param errorCode * @param line */ private void addModelError( ErrorCode errorCode, int line ) { addModelError( errorCode, line, null ); } /** * @param errorCode * @param line * @param details */ private void addModelError( ErrorCode errorCode, int line, String details ) { ParsingError me = new ParsingError( errorCode, this.definitionFile.getEditedFile(), line, details ); this.definitionFile.getParsingErrors().add( me ); } /** * Verifies whether starts with a keyword or is made up of this single keyword. * @param line the line * @param keyword the keyword * @return true if the keyword was found, false otherwise */ private boolean startsWith( String line, String keyword ) { return line.trim().matches( "(?i)^" + keyword + "((\\s.*)|$)" ); } }