/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.opus.function.builtin; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import org.apache.commons.io.FilenameUtils; import static org.novelang.parser.NodeKind.WORD_; import org.novelang.common.FileTools; import org.novelang.common.Location; import org.novelang.common.Problem; import org.novelang.common.Renderable; import org.novelang.common.SimpleTree; import org.novelang.common.StructureKind; import org.novelang.common.SyntacticTree; import org.novelang.common.tree.TreeTools; import org.novelang.common.tree.Treepath; import org.novelang.common.tree.TreepathTools; import org.novelang.designator.FragmentIdentifier; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.novella.Novella; import org.novelang.opus.CommandExecutionContext; import org.novelang.opus.function.CommandParameterException; import org.novelang.opus.function.builtin.insert.LevelHead; import org.novelang.opus.function.builtin.insert.PartCreator; import org.novelang.parser.NodeKind; import org.novelang.treemangling.DesignatorInterpreter; import org.novelang.treemangling.TreeManglingConstants; /** * @author Laurent Caillette */ public class InsertCommand extends AbstractCommand { private static final Logger LOGGER = LoggerFactory.getLogger( InsertCommand.class ); private final String fileName ; private final boolean recurse ; private final FileOrdering< ? > fileOrdering ; private final LevelHead levelHead; private final int levelAbove ; private final String styleName ; private final Iterable< FragmentIdentifier > fragmentIdentifiers ; public InsertCommand( final Location location, final String fileUrl, final boolean recurse, final FileOrdering fileOrdering, final LevelHead levelHead, final int levelAbove, final String styleName, final Iterable< FragmentIdentifier > fragmentIdentifiers ) { super( location ) ; this.fileName = fileUrl.substring( "file:".length() ) ; this.recurse = recurse ; this.levelHead = levelHead ; this.styleName = styleName ; if( fileOrdering == null ) { LOGGER.debug( "No file ordering set, using default" ) ; this.fileOrdering = FileOrdering.DEFAULT ; } else { this.fileOrdering = fileOrdering ; } Preconditions.checkArgument( levelAbove >= 0, "'levelabove' must be 0 or greater, is %d", levelAbove ) ; this.levelAbove = levelAbove ; this.fragmentIdentifiers = Iterables.unmodifiableIterable( fragmentIdentifiers ) ; } @Override public CommandExecutionContext evaluate( final CommandExecutionContext environment ) { final File insertedFile ; { final File candidateFile = new File( fileName ) ; if( candidateFile.isAbsolute() ) { insertedFile = candidateFile ; } else { insertedFile = new File( environment.getBookDirectory(), fileName ) ; } } if( insertedFile.isDirectory() ) { return evaluateMultiple( environment, insertedFile, recurse ) ; } else { return evaluateSingle( environment, insertedFile ) ; } } private CommandExecutionContext evaluateSingle( final CommandExecutionContext environment, final File insertedFile ) { LOGGER.debug( "Command ", this, " evaluating flatly on ", insertedFile ); final Novella rawNovella; try { rawNovella = new Novella( insertedFile, environment.getSourceCharset(), environment.getRenderingCharset() ) ; } catch( IOException e ) { return environment.addProblems( Lists.newArrayList( Problem.createProblem( e ) ) ) ; } final Renderable partWithRelocation = rawNovella.relocateResourcePaths( environment.getBaseDirectory() ) ; final SyntacticTree partTree = partWithRelocation.getDocumentTree() ; final SyntacticTree styleTree = createStyleTree( styleName ) ; Treepath< SyntacticTree > book = Treepath.create( environment.getDocumentTree() ) ; try { book = findLastLevel( book, levelAbove ) ; } catch ( CommandParameterException e ) { return environment.addProblem( Problem.createProblem( e ) ) ; } final Iterable< ? extends SyntacticTree > partTrees ; // TODO handle following cases altogether: fragments, createLevel. final boolean hasIdentifiers = fragmentIdentifiers.iterator().hasNext() ; if( partTree != null ) { final DesignatorInterpreter designatorInterpreter = new DesignatorInterpreter( Treepath.create( partTree ) ) ; if( hasIdentifiers ) { final AddIdentifiers addIdentifiers = new AddIdentifiers( designatorInterpreter, fragmentIdentifiers, getLocation(), true ) ; if( addIdentifiers.hasDesignatorProblem() ) { return environment.addProblems( addIdentifiers.getDesignatorProblems() ) ; } partTrees = removeHeadIfNeeded( levelHead, addIdentifiers.getPartTrees() ) ; } else { final SyntacticTree partTreeWithIdentifiers = designatorInterpreter.getEnrichedTreepath().getTreeAtStart() ; partTrees = removeHeadIfNeeded( levelHead, partTreeWithIdentifiers.getChildren() ) ; } if( levelHead == LevelHead.CREATE_LEVEL ) { book = createChapterFromPartFilename( book, insertedFile, partTrees, styleTree ) ; } else { for( SyntacticTree partChild : partTrees ) { if( styleTree != null ) { partChild = TreeTools.addFirst( partChild, styleTree ) ; } book = TreepathTools.addChildLast( book, partChild ).getStart() ; } } } return environment.update( book.getTreeAtStart() ).addProblems( rawNovella.getProblems() ) ; } private static SyntacticTree createStyleTree( final String styleName ) { if( null == styleName ) { return null ; } else { return new SimpleTree( NodeKind._STYLE, new SimpleTree( styleName ) ) ; } } /** * When inserting from several files, Identifiers make things more complex. * Identifier resolution must occur on all scanned Parts as a whole for preserving uniqueness * of the Identifiers. * (If the user doesn't want unique Identifiers he/she should insert from each Novella * explicitely, or use Tags.) * <p> * With {@link DesignatorInterpreter} alone, processing all scanned Parts at once drops * information about the originating Novella (and therefore the originating File) of each * inserted Fragment. This prevents level creation from working properly, and from * reporting Novella files in which collisions occur. * <p> * An approach for keeping originating Novella would be to decorate every Identifier-enabled * node with its origin before processing the whole with {@link DesignatorInterpreter}. * This will happen in the future, but not as a particular case here. * <p> * The approach of choice relies on after-the-fact conflict detection. * If requested Identifiers appear once and once only in the set of inserted Parts, * then processing each Novella individually gives the same result. Identifier unicity check * occurs by ensuring that requested Identifier appears once and once only in every * {@link DesignatorInterpreter}. */ private CommandExecutionContext evaluateMultiple( final CommandExecutionContext environment, final File insertedFile, final boolean recurse ) { LOGGER.debug( "Command ", this, " evaluating recursively on ", insertedFile ) ; final List< Problem > problems = Lists.newArrayList() ; final SyntacticTree styleTree = createStyleTree( styleName ) ; Treepath< SyntacticTree > book = Treepath.create( environment.getDocumentTree() ) ; final boolean hasIdentifiers = fragmentIdentifiers.iterator().hasNext() ; final Multimap< FragmentIdentifier, Novella> identifiedFragments ; if( hasIdentifiers ) { identifiedFragments = HashMultimap.create(); } else { identifiedFragments = null ; } try { book = findLastLevel( book, levelAbove ) ; final Iterable< File > partFiles = scanPartFiles( insertedFile, recurse ) ; final Map< File, Future<Novella> > futureParts = Maps.newHashMap() ; for( final File partFile : partFiles ) { final PartCreator partCreator = new PartCreator( partFile, environment.getSourceCharset(), environment.getRenderingCharset() ) ; futureParts.put( partFile, environment.getExecutorService().submit( partCreator ) ) ; } for( final File partFile : partFiles ) { Novella novella = null ; try { // environment.getExecutorService()... novella = futureParts.get( partFile ).get() ; Iterables.addAll( problems, novella.getProblems() ) ; } catch( ExecutionException e ) { problems.add( Problem.createProblem( ( Exception ) e.getCause() ) ) ; } catch( InterruptedException e ) { problems.add( Problem.createProblem( e ) ) ; } if( null != novella && null != novella.getDocumentTree() ) { final Novella relocatedNovella = novella.relocateResourcePaths( environment.getBaseDirectory() ) ; Iterables.addAll( problems, relocatedNovella.getProblems() ) ; final SyntacticTree partTree = relocatedNovella.getDocumentTree() ; final List< SyntacticTree > partChildren = Lists.newArrayList() ; final DesignatorInterpreter designatorInterpreter = new DesignatorInterpreter( Treepath.create( partTree ) ) ; if( designatorInterpreter.hasProblem() ) { Iterables.addAll( problems, designatorInterpreter.getProblems() ) ; } else { if( hasIdentifiers ) { for( final FragmentIdentifier fragmentIdentifier : fragmentIdentifiers ) { final Treepath< SyntacticTree > fragment = designatorInterpreter.get( fragmentIdentifier ) ; if( fragment != null ) { identifiedFragments.put( fragmentIdentifier, novella ) ; Iterables.addAll( partChildren, removeHeadIfNeeded( levelHead, fragment.getTreeAtEnd() ) ) ; } } } else { final SyntacticTree treeWithIdentifiers = designatorInterpreter.getEnrichedTreepath().getTreeAtStart() ; Iterables.addAll( partChildren, removeHeadIfNeeded( levelHead, treeWithIdentifiers.getChildren() ) ) ; } } if( levelHead == LevelHead.CREATE_LEVEL ) { book = createChapterFromPartFilename( book, partFile, partChildren, styleTree ) ; book = findLastLevel( book, levelAbove ) ; } else { for( SyntacticTree partChild : partChildren ) { if( styleTree != null ) { partChild = TreeTools.addFirst( partChild, styleTree ) ; } final Treepath< SyntacticTree > updatedBook = TreepathTools.addChildLast( book, partChild ) ; book = Treepath.create( updatedBook.getTreeAtStart() ) ; book = findLastLevel( book, levelAbove ) ; } } } } if( hasIdentifiers ) { Iterables.addAll( problems, createProblems( identifiedFragments, getLocation() ) ) ; } } catch( CommandParameterException e ) { problems.add( Problem.createProblem( e ) ) ; } return environment.update( book.getTreeAtStart() ).addProblems( problems ) ; } private static Treepath< SyntacticTree > findLastLevel( final Treepath< SyntacticTree > document, final int depth ) throws CommandParameterException { if( depth == 0 ) { return document ; } final SyntacticTree tree = document.getTreeAtEnd() ; final int lastChildIndex = tree.getChildCount() - 1 ; if( lastChildIndex < 0 ) { throw new CommandParameterException( "Found no child tree while seeking level " + depth ) ; } final SyntacticTree lastChild = tree.getChildAt( lastChildIndex ) ; if( lastChild.isOneOf( NodeKind._LEVEL ) ) { return findLastLevel( Treepath.create( document, lastChildIndex ), depth - 1 ) ; } else { throw new CommandParameterException( "Found no LEVEL as child tree" ) ; } } private static Treepath< SyntacticTree > createChapterFromPartFilename( Treepath< SyntacticTree > book, final File partFile, final Iterable< ? extends SyntacticTree > partTrees, final SyntacticTree styleTree ) { final SyntacticTree word = new SimpleTree( WORD_, new SimpleTree( FilenameUtils.getBaseName( partFile.getName() ) ) ) ; final SyntacticTree title = new SimpleTree( NodeKind.LEVEL_TITLE, word ) ; SyntacticTree chapterTree = TreeTools.addFirst( new SimpleTree( NodeKind._LEVEL, partTrees ), title ) ; if( styleTree != null ) { chapterTree = TreeTools.addFirst( chapterTree, styleTree ) ; } final Treepath< SyntacticTree > updatedBook = TreepathTools.addChildLast( book, chapterTree ) ; final SyntacticTree start = updatedBook.getTreeAtStart() ; book = Treepath.create( start ) ; return book ; } private Iterable< File > scanPartFiles( final File directory, final boolean recurse ) throws CommandParameterException { if( directory.isDirectory() ) { final Iterable< File > files ; try { final List< File > scannedFiles = FileTools.scanFiles( directory, StructureKind.NOVELLA.getFileExtensions(), recurse ); files = fileOrdering.sort( scannedFiles ) ; } catch ( FileOrdering.CriteriaException e ) { LOGGER.info( e, "Could not sort files from '", directory.getAbsolutePath(), "'" ) ; throw new CommandParameterException( "Could not sort files: " + e.getMessage() ) ; } if( LOGGER.isDebugEnabled() ) { final StringBuffer buffer = new StringBuffer( "Scan of '" + directory.getAbsolutePath() + "' found those files:" ) ; for( final File file : files ) { try { buffer.append( "\n " ).append( file.getCanonicalPath() ) ; } catch( IOException e ) { throw new RuntimeException( e ) ; } } LOGGER.debug( buffer.toString() ) ; } return files ; } else { throw new CommandParameterException( "Not a directory: '" + directory.getAbsolutePath() + "'" ) ; } } private static Iterable< Problem > createProblems( final Multimap< FragmentIdentifier, Novella> identifiedFragments, final Location location ) { final List< Problem > problems = Lists.newArrayList() ; for( final FragmentIdentifier fragmentIdentifier : identifiedFragments.keySet() ) { final Collection<Novella> novellas = identifiedFragments.get( fragmentIdentifier ) ; if( novellas == null ) { problems.add( Problem.createProblem( "Could not find " + fragmentIdentifier + " in any given Novella", location ) ) ; } else if( novellas.size() > 1 ) { problems.add( Problem.createProblem( "Identifier " + fragmentIdentifier + " found multiple times in:" + Joiner.on( "\n" ).join( novellas ) , location ) ) ; } } return problems ; } /** * Kind of function with multiple return values. */ private static class AddIdentifiers { private final List< Problem > designatorProblems = Lists.newArrayList() ; private final List< SyntacticTree > partTrees = Lists.newArrayList(); public AddIdentifiers( final DesignatorInterpreter designatorMapper, final Iterable< FragmentIdentifier > fragmentIdentifiers, final Location location, final boolean createProblemForUnmappedIdentifier ) { for( final FragmentIdentifier fragmentIdentifier : fragmentIdentifiers ) { final Treepath< SyntacticTree > fragmentTreepath = designatorMapper.get( fragmentIdentifier ) ; if( createProblemForUnmappedIdentifier && fragmentTreepath == null ) { designatorProblems.add( Problem.createProblem( "Cannot find: '" + fragmentIdentifier + "'", location ) ) ; } else { final SyntacticTree fragment = fragmentTreepath.getTreeAtEnd() ; partTrees.add( fragment ) ; } } Iterables.addAll( designatorProblems, designatorMapper.getProblems() ) ; } public boolean hasDesignatorProblem() { return ! designatorProblems.isEmpty() ; } public Iterable< Problem > getDesignatorProblems() { return designatorProblems ; } public Iterable< SyntacticTree > getPartTrees() { return partTrees ; } } private static Iterable< ? extends SyntacticTree > removeHeadIfNeeded( final LevelHead levelHead, final SyntacticTree tree ) { return removeHeadIfNeeded( levelHead, ImmutableList.of( tree ) ) ; } private static Iterable< ? extends SyntacticTree > removeHeadIfNeeded( final LevelHead levelHead, final Iterable< ? extends SyntacticTree > trees ) { if( levelHead == LevelHead.NO_HEAD ) { SyntacticTree level = null ; int levelCount = 0 ; int paragraphoidCount = 0 ; for( final SyntacticTree tree : trees ) { if( tree.getNodeKind() == NodeKind._LEVEL ) { level = tree ; levelCount ++ ; } if( TreeManglingConstants.PARAGRAPHOID_NODEKINDS.contains( tree.getNodeKind() ) ) { paragraphoidCount ++ ; } } if( levelCount == 1 && paragraphoidCount == 0 ) { final SyntacticTree cleanedTree = TreeTools.remove( level, LEVEL_DECORATION_PREDICATE ) ; return cleanedTree.getChildren() ; } } return trees ; } private static final Predicate<SyntacticTree> LEVEL_DECORATION_PREDICATE = new Predicate< SyntacticTree >() { @Override public boolean apply( final SyntacticTree syntacticTree ) { return TreeManglingConstants.LEVEL_DECORATION_NODEKINDS .contains( syntacticTree.getNodeKind() ) ; } } ; @Override public String toString() { return "InsertCommand{" + "fileName='" + fileName + '\'' + ", recurse=" + recurse + ", levelHead=" + levelHead + ", styleName='" + styleName + '\'' + ", fragmentIdentifiers=" + fragmentIdentifiers + '}' ; } }