com.ben12.reta.util.RETAAnalysis.java Source code

Java tutorial

Introduction

Here is the source code for com.ben12.reta.util.RETAAnalysis.java

Source

// Package : com.ben12.reta.util
// File : RETAAnalysis.java
// 
// Copyright (C) 2014 Benot Moreau (ben.12)
//
// This file is part of RETA (Requirement Engineering Traceability Analysis).
//
// RETA is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// RETA 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 RETA.  If not, see <http://www.gnu.org/licenses/>.

package com.ben12.reta.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javafx.beans.property.DoubleProperty;
import javafx.stage.Window;

import javax.validation.constraints.NotNull;

import net.sf.jett.transform.ExcelTransformer;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.ini4j.Profile.Section;
import org.ini4j.Wini;

import com.ben12.reta.constraints.PathExists;
import com.ben12.reta.constraints.PathExists.KindOfPath;
import com.ben12.reta.model.InputRequirementSource;
import com.ben12.reta.model.Requirement;
import com.ben12.reta.view.control.MessageDialog;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.io.Files;

// TODO : think of a better implementation
/**
 * @author Benot Moreau (ben.12)
 */
public final class RETAAnalysis {
    /** Buffer size (2 Mo). */
    private static final int BUFFER_SIZE = 2 * 1024 * 1024;

    public static final String REQUIREMENT_SOURCES = "requirementSources";

    private static RETAAnalysis instance = null;

    private final Logger logger = Logger.getLogger(RETAAnalysis.class.getName());

    private final Map<String, InputRequirementSource> requirementSources = new LinkedHashMap<>();

    private File config = null;

    @NotNull(message = "invalid.path")
    @com.ben12.reta.constraints.Path
    @PathExists(kind = KindOfPath.DIRECTORY, parent = true)
    private String output = null;

    // TODO maybe useful to see unknown references and mismatch versions
    // private final Comparator<Requirement> reqCompId = (req1, req2) -> req1.getId().compareTo(req2.getId());

    private RETAAnalysis() {
    }

    public static RETAAnalysis getInstance() {
        if (instance == null) {
            instance = new RETAAnalysis();
        }
        return instance;
    }

    /**
     * @return the requirementSources
     */
    public Map<String, InputRequirementSource> getRequirementSources() {
        return requirementSources;
    }

    /**
     * @return the config
     */
    public File getConfig() {
        return config;
    }

    /**
     * @return the output
     */
    public String getOutput() {
        return output;
    }

    /**
     * @param output
     *            the output to set
     */
    public void setOutput(String output) {
        this.output = output;
    }

    public void configure(File iniFile) {
        config = iniFile;

        try {
            requirementSources.clear();

            Wini ini = new Wini();
            ini.getConfig().setFileEncoding(Charset.forName("CP1252"));
            ini.load(iniFile);

            String output = ini.get("GENERAL", "output");
            if (output != null) {
                Path outPath = Paths.get(output);
                this.output = outPath.normalize().toString();
                String fileName = outPath.getFileName().toString();
                if (!"xlsx".equals(Files.getFileExtension(fileName))) {
                    logger.warning("output extension changed for xlsx");
                    Path parent = outPath.getParent();
                    if (parent == null) {
                        this.output = Files.getNameWithoutExtension(fileName) + ".xlsx";
                    } else {
                        this.output = parent.resolve(Files.getNameWithoutExtension(fileName) + ".xlsx").toString();
                    }
                }
            } else {
                this.output = Paths
                        .get(iniFile.getParent(), Files.getNameWithoutExtension(iniFile.getName()) + ".xlsx")
                        .toString();
            }

            Map<InputRequirementSource, List<String>> coversMap = new LinkedHashMap<>();

            String attributesStr = ini.get("GENERAL", "requirement.attributes");
            Iterable<String> attributes = Collections.emptyList();
            if (!Strings.isNullOrEmpty(attributesStr)) {
                attributes = Splitter.on(',').trimResults().omitEmptyStrings().split(attributesStr);
            }

            String refAttributesStr = ini.get("GENERAL", "references.attributes");
            Iterable<String> refAttributes = Collections.emptyList();
            if (!Strings.isNullOrEmpty(refAttributesStr)) {
                refAttributes = Splitter.on(',').trimResults().omitEmptyStrings().split(refAttributesStr);
            }

            String documentsStr = ini.get("GENERAL", "inputs");
            if (!Strings.isNullOrEmpty(documentsStr)) {
                Iterable<String> documents = Splitter.on(',').trimResults().omitEmptyStrings().split(documentsStr);

                for (String doc : documents) {
                    Section section = ini.get(doc);
                    if (section == null) {
                        logger.warning("No section defined for input " + doc);
                        continue;
                    }
                    String source = section.get("path", "");
                    String filter = section.get("filter", "");
                    if (!source.isEmpty()) {
                        InputRequirementSource requirementSource = new InputRequirementSource(doc, source, filter);

                        try {
                            String startRegex = section.get("requirement.start.regex", "");
                            String endRegex = section.get("requirement.end.regex", "");
                            Integer textIndex = section.get(
                                    "requirement.start." + Requirement.ATTRIBUTE_TEXT + ".index", Integer.class, 0);
                            Integer idIndex = section.get(
                                    "requirement.start." + Requirement.ATTRIBUTE_ID + ".index", Integer.class,
                                    null);
                            Integer versionIndex = section.get(
                                    "requirement.start." + Requirement.ATTRIBUTE_VERSION + ".index", Integer.class,
                                    null);

                            if (startRegex.isEmpty()) {
                                if (!endRegex.isEmpty()) {
                                    logger.severe("requirement.start.regex is mandatory for section " + doc);
                                }
                            } else if (idIndex == null) {
                                logger.severe("requirement.start.Id.index is mandatory for section " + doc);
                                idIndex = 0;
                            }

                            requirementSource.setReqStart(startRegex);
                            requirementSource.setReqEnd(endRegex);
                            requirementSource.getAttributesGroup().put(Requirement.ATTRIBUTE_TEXT, textIndex);
                            requirementSource.getAttributesGroup().put(Requirement.ATTRIBUTE_ID, idIndex);
                            if (versionIndex != null) {
                                requirementSource.getAttributesGroup().put(Requirement.ATTRIBUTE_VERSION,
                                        versionIndex);
                            }

                            for (String att : attributes) {
                                Integer attIndex = section.get("requirement.start." + att + ".index", Integer.class,
                                        null);
                                if (attIndex != null) {
                                    requirementSource.getAttributesGroup().put(att, attIndex);
                                }
                            }

                            String coversStr = section.get("covers", "");
                            List<String> covers = Splitter.on(',').trimResults().omitEmptyStrings()
                                    .splitToList(coversStr);
                            coversMap.put(requirementSource, covers);

                            String refRegex = section.get("requirement.ref.regex");
                            if (!Strings.isNullOrEmpty(refRegex)) {
                                Integer refIdIndex = section.get(
                                        "requirement.ref." + Requirement.ATTRIBUTE_ID + ".index", Integer.class,
                                        null);
                                Integer refVersionIndex = section.get(
                                        "requirement.ref." + Requirement.ATTRIBUTE_VERSION + ".index",
                                        Integer.class, null);

                                if (Strings.isNullOrEmpty(refRegex)) {
                                    logger.severe("requirement.ref.regex is mandatory for section " + doc);
                                    continue;
                                } else if (refIdIndex == null) {
                                    logger.severe("requirement.ref.Id.index is mandatory for section " + doc);
                                    continue;
                                }

                                requirementSource.setReqRef(refRegex);
                                requirementSource.getRefAttributesGroup().put(Requirement.ATTRIBUTE_ID, refIdIndex);
                                if (refVersionIndex != null) {
                                    requirementSource.getRefAttributesGroup().put(Requirement.ATTRIBUTE_VERSION,
                                            refVersionIndex);
                                }

                                for (String att : refAttributes) {
                                    Integer attIndex = section.get("requirement.ref." + att + ".index",
                                            Integer.class, null);
                                    if (attIndex != null) {
                                        requirementSource.getRefAttributesGroup().put(att, attIndex);
                                    }
                                }
                            } else if (!covers.isEmpty()) {
                                logger.warning("requirement.ref.regex is mandatory for section " + doc
                                        + " with covers defined.");
                            }

                            requirementSources.put(doc, requirementSource);
                        } catch (IllegalArgumentException e) {
                            logger.log(Level.SEVERE, "Invalid value for section " + doc, e);
                        }
                    } else {
                        logger.warning("No source defined for input " + doc);
                    }
                }

                coversMap.entrySet().stream().forEach(entry -> {
                    InputRequirementSource requirementSource = entry.getKey();
                    entry.getValue().stream().forEach(cover -> {
                        InputRequirementSource coverRequirementSource = requirementSources.get(cover);
                        if (coverRequirementSource != null) {
                            requirementSource.getCovers().add(coverRequirementSource);
                        } else {
                            logger.warning(requirementSource.getName() + " covers an unknown input " + cover);
                        }
                    });
                });
            } else {
                logger.warning("No inputs defined in GENERAL section.");
            }
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Invalid config file " + iniFile, e);
            // TODO show error as dialog or dimmer panel
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Error reading config file " + iniFile, e);
            // TODO show error as dialog or dimmer panel
        }
    }

    public void saveConfig() {
        saveConfig(config);
    }

    public void saveConfig(File iniFile) {
        Wini ini = new Wini();
        ini.getConfig().setFileEncoding(Charset.forName("CP1252"));

        try {
            Section generalSection = ini.add("GENERAL");

            generalSection.add("output", output.toString());

            List<String> inputs = new ArrayList<>();
            Set<String> reqAttributes = new HashSet<>();
            Set<String> refAttributes = new HashSet<>();

            for (InputRequirementSource requirementSource : requirementSources.values()) {
                String name = requirementSource.getName();
                inputs.add(name);

                Section sourceSection = ini.add(name);

                sourceSection.put("path", requirementSource.getSourcePath());
                sourceSection.put("filter", requirementSource.getFilter());

                String reqStart = requirementSource.getReqStart();
                if (!Strings.isNullOrEmpty(reqStart)) {
                    Map<String, Integer> reqAttributesGroup = requirementSource.getAttributesGroup();
                    reqAttributes.addAll(reqAttributesGroup.keySet());

                    sourceSection.put("requirement.start.regex", reqStart);

                    for (Entry<String, Integer> reqAttributeGroup : reqAttributesGroup.entrySet()) {
                        if (reqAttributeGroup.getValue() != null) {
                            sourceSection.put("requirement.start." + reqAttributeGroup.getKey() + ".index",
                                    reqAttributeGroup.getValue());
                        }
                    }

                    String reqEnd = requirementSource.getReqEnd();
                    if (!Strings.isNullOrEmpty(reqEnd)) {
                        sourceSection.put("requirement.end.regex", reqEnd);
                    }
                }

                String reqRef = requirementSource.getReqRef();
                if (!Strings.isNullOrEmpty(reqRef)) {
                    Map<String, Integer> refAttributesGroup = requirementSource.getRefAttributesGroup();
                    refAttributes.addAll(refAttributesGroup.keySet());

                    sourceSection.put("requirement.ref.regex", reqRef);

                    for (Entry<String, Integer> refAttributeGroup : refAttributesGroup.entrySet()) {
                        if (refAttributeGroup.getValue() != null) {
                            sourceSection.put("requirement.ref." + refAttributeGroup.getKey() + ".index",
                                    refAttributeGroup.getValue());
                        }
                    }

                    sourceSection.put("covers", requirementSource.getCovers().stream()
                            .map(InputRequirementSource::getName).collect(Collectors.joining(",")));
                }
            }

            generalSection.put("inputs", inputs.stream().collect(Collectors.joining(",")));

            List<String> excludes = Arrays.asList(Requirement.ATTRIBUTE_ID, Requirement.ATTRIBUTE_TEXT,
                    Requirement.ATTRIBUTE_VERSION);
            generalSection.put("requirement.attributes",
                    reqAttributes.stream().filter(a -> !excludes.contains(a)).collect(Collectors.joining(",")));
            generalSection.put("references.attributes",
                    refAttributes.stream().filter(a -> !excludes.contains(a)).collect(Collectors.joining(",")));

            ini.store(iniFile);
            config = iniFile;
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Invalid config file " + iniFile, e);
            // TODO show error as dialog or dimmer panel
        }
    }

    public void parse(DoubleProperty progress) throws IOException {
        final IOException[] ex = { null };
        final AtomicInteger count = new AtomicInteger(0);

        requirementSources.values().parallelStream().forEach(requirementSource -> {
            try {
                parse(requirementSource);
                synchronized (progress) {
                    progress.set((double) count.incrementAndGet() / requirementSources.size());
                }
            } catch (IOException e) {
                ex[0] = new IOException(
                        "Parsing source \"" + requirementSource.getName() + "\": " + e.getLocalizedMessage(), e);
            }
        });

        if (ex[0] != null) {
            throw ex[0];
        }
    }

    private void parse(InputRequirementSource requirementSource) throws IOException {
        parse(requirementSource, null, Integer.MAX_VALUE);
    }

    public void parse(InputRequirementSource requirementSource, StringBuilder sourceText, int limit)
            throws IOException {
        logger.info("Start parsing " + requirementSource.getName());

        requirementSource.clear();

        Pattern patternStart = null;
        Pattern patternEnd = null;
        Pattern patternRef = null;
        if (!Strings.isNullOrEmpty(requirementSource.getReqRef())) {
            patternRef = Pattern.compile(requirementSource.getReqRef(), Pattern.MULTILINE);
        }
        if (!Strings.isNullOrEmpty(requirementSource.getReqStart())) {
            patternStart = Pattern.compile(requirementSource.getReqStart(), Pattern.MULTILINE);
            if (!Strings.isNullOrEmpty(requirementSource.getReqEnd())) {
                patternEnd = Pattern.compile(requirementSource.getReqEnd(), Pattern.MULTILINE);
            }

            parseMultiRequirementByFile(requirementSource, patternStart, patternEnd, patternRef, sourceText, limit);
        } else {
            parseReferencesInFiles(requirementSource, patternRef, sourceText, limit);
        }

        logger.info("End parsing " + requirementSource.getName());
    }

    /**
     * @param newRequirementSource
     * @param newPatternRef
     * @throws IOException
     */
    private void parseReferencesInFiles(InputRequirementSource requirementSource, Pattern patternRef,
            StringBuilder sourceText, int limit) throws IOException {
        final ConcatReader reader = getReader(requirementSource);
        Path root = Paths.get(requirementSource.getSourcePath());
        if (!root.isAbsolute()) {
            Path cfgRoot = config.getAbsoluteFile().getParentFile().toPath();
            root = cfgRoot.resolve(root);
        }
        final CharBuffer buffer = CharBuffer.allocate(BUFFER_SIZE);
        StringBuilder builder = new StringBuilder(3 * BUFFER_SIZE);
        int r = reader.read(buffer);
        while (r >= 0 && (limit == Integer.MAX_VALUE || limit > 0)) {
            buffer.flip();
            builder.append(buffer, 0, Math.min(limit, r));
            if (sourceText != null) {
                buffer.rewind();
                sourceText.append(buffer, 0, Math.min(limit, r));
            }
            buffer.clear();
            if (limit != Integer.MAX_VALUE) {
                limit -= r;
            }

            Path prevPath = reader.getCurrentPath();
            r = reader.read(buffer);
            Path currentPath = reader.getCurrentPath();

            // If all file content read
            if (!prevPath.equals(currentPath)) {
                String relPath = root.relativize(prevPath).toString();
                Requirement requirement = new Requirement(requirementSource);
                requirement.setId(relPath);
                requirement.setText(relPath);

                Matcher matcherRef = patternRef.matcher(builder);
                parseReferences(requirementSource, requirement, matcherRef);

                if (requirement.getReferenceCount() > 0) {
                    requirementSource.getRequirements().add(requirement);
                }

                builder.setLength(0);
            }
        }
    }

    /**
     * @param requirementSource
     * @param patternStart
     * @param patternEnd
     * @param patternRef
     * @throws IOException
     */
    private void parseMultiRequirementByFile(InputRequirementSource requirementSource, Pattern patternStart,
            Pattern patternEnd, Pattern patternRef, StringBuilder sourceText, int limit) throws IOException {
        boolean requirementStarted = false;
        Requirement requirement = null;
        final ConcatReader reader = getReader(requirementSource);
        final CharBuffer buffer = CharBuffer.allocate(BUFFER_SIZE);
        StringBuilder builder = new StringBuilder(3 * BUFFER_SIZE);
        int r = reader.read(buffer);
        while (r >= 0 && (limit == Integer.MAX_VALUE || limit > 0)) {
            buffer.flip();
            builder.append(buffer, 0, Math.min(limit, r));
            if (sourceText != null) {
                buffer.rewind();
                sourceText.append(buffer, 0, Math.min(limit, r));
            }
            buffer.clear();
            if (limit != Integer.MAX_VALUE) {
                limit -= r;
            }

            Path path = reader.getCurrentPath();
            r = reader.read(buffer);
            Path newPath = reader.getCurrentPath();

            Matcher matcherStart = patternStart.matcher(builder);
            Matcher matcherEnd = (patternEnd != null ? patternEnd.matcher(builder) : null);

            while (matcherStart.find(0)) {
                requirementStarted = true;

                // find start is at the end of buffer and we are not at the eof.
                if (matcherStart.end() == builder.length() && r >= 0 && path.equals(newPath)) {
                    // requirement could be partial
                    break;
                }

                boolean endMatch = false;
                if (matcherEnd != null) {
                    endMatch = matcherEnd.find(matcherStart.end());
                }

                if (endMatch) {
                    int pos = matcherStart.start();
                    int endPos = matcherEnd.start();
                    // search for last requirement start before the requirement end.
                    String req = matcherStart.group();
                    boolean hasNextStart = matcherStart.find(matcherStart.end());
                    while (hasNextStart && matcherStart.start() < endPos) {
                        logger.warning(requirementSource.getName() + " (" + path
                                + "): \nIgnore matching requirement without end :" + req);
                        pos = matcherStart.start();
                        req = matcherStart.group();
                        hasNextStart = matcherStart.find(matcherStart.end());
                    }
                    matcherStart.find(pos);
                }

                if (requirement != null && patternRef != null) {
                    // search for references between last requirement end and new requirement start
                    Matcher matcherRef = patternRef.matcher(builder);
                    matcherRef.region(0, matcherStart.start());
                    parseReferences(requirementSource, requirement, matcherRef);
                    builder.replace(0, matcherStart.start(), "");
                    matcherStart.find(0);
                }

                if (matcherEnd == null || matcherEnd.find(matcherStart.end())) {
                    int endPos = matcherStart.end();
                    int endEndPos = matcherStart.end();
                    if (matcherEnd != null) {
                        endPos = matcherEnd.start();
                        endEndPos = matcherEnd.end();
                    }

                    String reqContent = builder.substring(matcherStart.end(), endPos);

                    requirement = new Requirement(requirementSource);
                    requirement.setContent(reqContent);

                    for (Map.Entry<String, Integer> attEntry : requirementSource.getAttributesGroup().entrySet()) {
                        String attName = attEntry.getKey();
                        Integer group = attEntry.getValue();
                        if (group != null && group <= matcherStart.groupCount()) {
                            String reqAtt = Strings.nullToEmpty(matcherStart.group(group));
                            requirement.putAttribute(attName, reqAtt);
                        }
                    }

                    if (patternRef != null) {
                        Matcher matcherRef = patternRef.matcher(reqContent);
                        parseReferences(requirementSource, requirement, matcherRef);
                    }

                    if (!requirementSource.getRequirements().add(requirement)) {
                        logger.warning(requirementSource.getName() + " (" + path
                                + "): \nIgnore duplicate matching requirement :" + matcherStart.group());
                    }

                    builder.replace(0, endEndPos, "");
                    requirementStarted = false;
                } else {
                    break;
                }
            }

            // if no requirement from 2 buffer size
            if (!requirementStarted && builder.length() >= 2 * BUFFER_SIZE
                    && (requirement == null || patternRef == null)) {
                builder.delete(0, builder.length() - BUFFER_SIZE);
            }
        }

        if (requirement != null && patternRef != null) {
            // search for references between last requirement end and end of file
            Matcher matcherRef = patternRef.matcher(builder);
            parseReferences(requirementSource, requirement, matcherRef);
            builder.setLength(0);
        }
    }

    private void parseReferences(InputRequirementSource requirementSource, Requirement requirement,
            Matcher matcherRef) {
        while (matcherRef.find()) {
            String refText = Strings.nullToEmpty(matcherRef.group(0));

            Requirement reference = new Requirement();
            reference.setText(refText);

            for (Map.Entry<String, Integer> attEntry : requirementSource.getRefAttributesGroup().entrySet()) {
                String attName = attEntry.getKey();
                Integer group = attEntry.getValue();
                if (group != null && group <= matcherRef.groupCount()) {
                    String reqAtt = Strings.nullToEmpty(matcherRef.group(group));
                    reference.putAttribute(attName, reqAtt);
                }
            }

            requirement.addReference(reference);
        }
    }

    private ConcatReader getReader(InputRequirementSource requirementSource) throws IOException {
        ConcatReader concatReader = new ConcatReader();
        Path srcPath = Paths.get(requirementSource.getSourcePath());
        if (!srcPath.isAbsolute()) {
            Path root = config.getAbsoluteFile().getParentFile().toPath();
            srcPath = root.resolve(srcPath);
        }
        if (srcPath.toFile().isFile()) {
            concatReader.add(srcPath);
        } else {
            Pattern patternfilter = null;
            String filter = requirementSource.getFilter();
            if (!Strings.isNullOrEmpty(filter)) {
                patternfilter = Pattern.compile(filter);
            }
            fillReaders(concatReader, srcPath, srcPath, patternfilter);
        }
        return concatReader;
    }

    private void fillReaders(ConcatReader concatReader, Path base, Path root, Pattern filter) throws IOException {
        DirectoryStream<Path> stream = java.nio.file.Files.newDirectoryStream(root);
        Iterator<Path> iterator = stream.iterator();
        while (iterator.hasNext()) {
            Path path = iterator.next();
            if (path.toFile().isFile()) {
                if (filter == null || filter.matcher(base.relativize(path).toString()).find()) {
                    concatReader.add(path);
                }
            } else if (path.startsWith(root) && !path.equals(root)) {
                fillReaders(concatReader, base, path, filter);
            }
        }
    }

    public void analyse() throws IOException {
        for (InputRequirementSource source : requirementSources.values()) {
            logger.info("Start analyse " + source.getName());

            List<InputRequirementSource> covers = source.getCovers();
            TreeSet<Requirement> reqSource = source.getRequirements();
            for (InputRequirementSource coverSource : covers) {
                TreeSet<Requirement> reqCover = coverSource.getRequirements();
                TreeSet<Requirement> reqCoverred = new TreeSet<>(reqCover);
                for (Requirement req : reqSource) {
                    Set<Requirement> realReqCovers = new TreeSet<>();
                    Iterable<Requirement> refs = req.getReferences();
                    for (Requirement reqRef : refs) {
                        Requirement found = reqCover.ceiling(reqRef);
                        if (found != null && found.equals(reqRef)) {
                            reqCoverred.remove(found);
                            realReqCovers.add(found);
                            found.addReferredBy(req);
                        }
                    }
                    for (Requirement realReqCover : realReqCovers) {
                        req.addReference(realReqCover);
                    }
                }
                double coverage = (double) (reqCover.size() - reqCoverred.size()) / reqCover.size();
                coverSource.getCoversBy().put(source, coverage);
            }

            logger.info("End analyse " + source.getName());
        }
    }

    public void writeExcel(Window parent) throws IOException, InvalidFormatException {
        logger.info("Start write excel output");

        Path outputFile = Paths.get(output);
        if (!outputFile.isAbsolute()) {
            Path root = config.getAbsoluteFile().getParentFile().toPath();
            outputFile = root.resolve(outputFile);
        }

        // test using template
        InputStream is = getClass().getResourceAsStream("/com/ben12/reta/resources/template/template.xlsx");
        ExcelTransformer transformer = new ExcelTransformer();
        List<String> sheetNames = new ArrayList<>();
        List<String> sheetTemplateNames = new ArrayList<>();
        for (InputRequirementSource requirementSource : requirementSources.values()) {
            sheetTemplateNames.add("DOCUMENT");
            sheetTemplateNames.add("COVERAGE");
            sheetNames.add(requirementSource.getName());
            sheetNames.add(requirementSource.getName() + " coverage");
        }

        List<Map<String, Object>> sheetValues = new ArrayList<>();
        for (InputRequirementSource source : requirementSources.values()) {
            Map<String, Object> values = new HashMap<>();
            values.put("source", source);
            values.put("null", null);
            values.put("line", "\n");

            Set<String> attributes = new LinkedHashSet<>();
            attributes.add(Requirement.ATTRIBUTE_ID);
            if (source.getAttributesGroup().containsKey(Requirement.ATTRIBUTE_VERSION)) {
                attributes.add(Requirement.ATTRIBUTE_VERSION);
            }
            attributes.addAll(source.getAttributesGroup().keySet());
            attributes.remove(Requirement.ATTRIBUTE_TEXT);
            values.put("attributes", attributes);

            Set<String> refAttributes = new LinkedHashSet<>();
            refAttributes.add(Requirement.ATTRIBUTE_ID);
            if (source.getRefAttributesGroup().containsKey(Requirement.ATTRIBUTE_VERSION)) {
                refAttributes.add(Requirement.ATTRIBUTE_VERSION);
            }
            refAttributes.addAll(source.getRefAttributesGroup().keySet());
            refAttributes.remove(Requirement.ATTRIBUTE_TEXT);
            values.put("refAttributes", refAttributes);

            sheetValues.add(values);
            sheetValues.add(values);
        }

        Workbook wb = transformer.transform(is, sheetTemplateNames, sheetNames, sheetValues);
        int sheetCount = wb.getNumberOfSheets();
        for (int i = 0; i < sheetCount; i++) {
            Sheet sheet = wb.getSheetAt(i);
            int columns = 0;
            for (int j = 0; j <= sheet.getLastRowNum(); j++) {
                Row row = sheet.getRow(j);
                if (row != null) {
                    row.setHeight((short) -1);
                    columns = Math.max(columns, row.getLastCellNum() + 1);
                }
            }
            for (int j = 0; j < columns; j++) {
                sheet.autoSizeColumn(j);
            }
        }

        try (FileOutputStream fos = new FileOutputStream(outputFile.toFile())) {
            wb.write(fos);
        } catch (FileNotFoundException e) {
            int confirm = MessageDialog.showQuestionMessage(null, "Excel output file must be closed.");

            if (confirm == MessageDialog.OK_OPTION) {
                try (FileOutputStream fos = new FileOutputStream(outputFile.toFile())) {
                    wb.write(fos);
                } catch (IOException e2) {
                    throw e2;
                }
            } else {
                throw e;
            }
        }

        logger.info("End write excel output");
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        for (InputRequirementSource reqSource : requirementSources.values()) {
            builder.append(reqSource.toString());
            builder.append('\n');
        }
        return builder.toString();
    }
}