/* * Demoiselle Framework * Copyright (C) 2013 SERPRO * ---------------------------------------------------------------------------- * This file is part of Demoiselle Framework. * * Demoiselle Framework is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License version 3 * as published by the Free Software Foundation. * * 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 Lesser General Public License version 3 * along with this program; if not, see <http://www.gnu.org/licenses/> * or write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301, USA. * ---------------------------------------------------------------------------- * Este arquivo é parte do Framework Demoiselle. * * O Framework Demoiselle é um software livre; você pode redistribuí-lo e/ou * modificá-lo dentro dos termos da GNU LGPL versão 3 como publicada pela Fundação * do Software Livre (FSF). * * Este programa é distribuído na esperança que possa ser útil, mas SEM NENHUMA * GARANTIA; sem uma garantia implícita de ADEQUAÇÃO a qualquer MERCADO ou * APLICAÇÃO EM PARTICULAR. Veja a Licença Pública Geral GNU/LGPL em português * para maiores detalhes. * * Você deve ter recebido uma cópia da GNU LGPL versão 3, sob o título * "LICENCA.txt", junto com esse programa. Se não, acesse <http://www.gnu.org/licenses/> * ou escreva para a Fundação do Software Livre (FSF) Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02111-1301, USA. */ /* * Demoiselle Framework * Copyright (C) 2013 SERPRO * ---------------------------------------------------------------------------- * This file is part of Demoiselle Framework. * * Demoiselle Framework is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License version 3 * as published by the Free Software Foundation. * * 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 Lesser General Public License version 3 * along with this program; if not, see <http://www.gnu.org/licenses/> * or write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301, USA. * ---------------------------------------------------------------------------- * Este arquivo é parte do Framework Demoiselle. * * O Framework Demoiselle é um software livre; você pode redistribuí-lo e/ou * modificá-lo dentro dos termos da GNU LGPL versão 3 como publicada pela Fundação * do Software Livre (FSF). * * Este programa é distribuído na esperança que possa ser útil, mas SEM NENHUMA * GARANTIA; sem uma garantia implícita de ADEQUAÇÃO a qualquer MERCADO ou * APLICAÇÃO EM PARTICULAR. Veja a Licença Pública Geral GNU/LGPL em português * para maiores detalhes. * * Você deve ter recebido uma cópia da GNU LGPL versão 3, sob o título * "LICENCA.txt", junto com esse programa. Se não, acesse <http://www.gnu.org/licenses/> * ou escreva para a Fundação do Software Livre (FSF) Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02111-1301, USA. */ package br.gov.frameworkdemoiselle.behave.internal.parser; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; import br.gov.frameworkdemoiselle.behave.config.BehaveConfig; import br.gov.frameworkdemoiselle.behave.controller.BehaveContext; import br.gov.frameworkdemoiselle.behave.exception.BehaveException; import br.gov.frameworkdemoiselle.behave.internal.filter.ScenarioFilter; import br.gov.frameworkdemoiselle.behave.internal.filter.StoryOrScenarioFilter; import br.gov.frameworkdemoiselle.behave.internal.filter.StoryFilter; import br.gov.frameworkdemoiselle.behave.internal.util.RegularExpressionUtil; import br.gov.frameworkdemoiselle.behave.message.BehaveMessage; /** * * @author SERPRO * */ public class StoryConverter { private static final String LINE_BREAK_TOKEN = "\n"; private static BehaveMessage bm = new BehaveMessage(BehaveConfig.MESSAGEBUNDLE); /* * Definições: story=arquivo com um ou mais cenários ; * cenário=identificador+sentenças Algoritmo: Converter todas os cenários de * todos as stories (textos extraídos dos arquivos) para objetos do tipo * Scenario (declaração, identificação e List de sentenças) Varrer a lista * de sentenças para ver quais correspondem à identificação de cenários * (reuso de cenário) Para cada sentença identificada como chamando a um * cenário, caso este não tenha sido convertido, deve ser convertido antes * de ser reutilizado (cenário que reusa um cenário que reutiliza outro * cenário) Os parâmetros formais devem ser substituídos pelos reais (da * chamada) antes de serem reutilizados Converter os objetos Scenario de * volta para um texto correspondente ao arquivo convertido */ /** * Gera stories a partir de stories originais, substituindo referências à * stories pelo conteúdo das mesmas, além de realizar a adequada * substituição dos parâmetros * * @param pStories * : Map com o path do arquivo e o conteúdo * @return Map com o path do arquivo e o conteúdo convertido * @throws IOException */ public static Map<String, String> convertReusedScenarios(Map<String, String> stories) throws IOException { Map<String, String> convertedStories = new LinkedHashMap<String, String>(); // Pega as definições das histórias (tudo que vem antes do primeiro // cenário) Map<String, String> storyDefinitions = extractStoryDefinitions(stories); // Cria uma lista contendo o identificador da história e uma lista com // todos os cenários Map<String, List<Scenario>> scenarios = extractScenarios(stories); // Faz a reutilização dos cenários reuseScenario(scenarios); // Evita que os cenários reutilizáveis sejam executados, mesmo que // nenhum cenário concreto tenha sido encontrado. for (String key : scenarios.keySet()) { List<Scenario> actual = scenarios.get(key); List<Scenario> toRemove = new ArrayList<Scenario>(); for (Scenario cenario : actual) { if (cenario.getReusable()) { toRemove.add(cenario); } } actual.removeAll(toRemove); } // Converte os objetos dos cenários (com reuso) em histórias novamente convertedStories = scenariosToStories(storyDefinitions, scenarios); return convertedStories; } private static Map<String, List<Scenario>> extractScenarios(Map<String, String> stories) { Map<String, List<Scenario>> scenarios = new LinkedHashMap<String, List<Scenario>>(); for (String storyPath : stories.keySet()) { scenarios.put(storyPath, extractScenarios(stories.get(storyPath))); } // Verifica assinatura verifyDuplicateScenarios(scenarios); return scenarios; } /** * Verifica se existe algum cenário com a assinatura identica, se tiver para * o teste, não pode existir por causa do reuso que * * @param scenariosMap */ private static void verifyDuplicateScenarios(Map<String, List<Scenario>> scenariosMap) { if (BehaveConfig.getParser_ErroDuplicateScenarios()) { ArrayList<String> scenariosSignature = new ArrayList<String>(); for (String story : scenariosMap.keySet()) { List<Scenario> scenarios = scenariosMap.get(story); for (Scenario scenario : scenarios) { if (!scenariosSignature.contains(scenario.getIdentificationWithoutParametersName())) { scenariosSignature.add(scenario.getIdentificationWithoutParametersName()); } else { throw new BehaveException(bm.getString("exception-scenario-duplicated", scenario.getIdentification())); } } } } } /** * * @param stories * Todas as histórias que serão utilizadas * @return retorna um mapa contendo o arquivo e a */ private static Map<String, String> extractStoryDefinitions(Map<String, String> stories) { Map<String, String> storyDefinitions = new LinkedHashMap<String, String>(); for (String storyPath : stories.keySet()) { storyDefinitions.put(storyPath, extractStoryDefinition(stories.get(storyPath))); } return storyDefinitions; } /** * Retira o "Como um: / Eu quero: / De modo que: /" da história * * @param storyContent * @return retorna somente o conteúdo "Como um..." */ private static String extractStoryDefinition(String storyContent) { String[] scenarioTokens = storyContent.split(LINE_BREAK_TOKEN); String storyDefinition = ""; for (int i = 0; i < scenarioTokens.length; i++) { String scenarioToken = scenarioTokens[i]; if (RegularExpressionUtil.matches(BehaveConfig.getParser_IdentificationScenarioPattern(), scenarioToken.trim())) { return storyDefinition; } // Remove os comentários da história String st = removeComment(scenarioToken); storyDefinition += st.equals("") ? "" : st + LINE_BREAK_TOKEN; } return storyDefinition; } /** * Método que retira os comentários da história * * @param scenarioToken * @return */ private static String removeComment(String scenarioToken) { return !Pattern.compile("^(!--)(.*)").matcher(scenarioToken).find() ? (scenarioToken) : ""; } private static List<Scenario> extractScenarios(String storyContent) { List<Scenario> scenarios = new ArrayList<Scenario>(); String[] scenarioTokens = storyContent.split(LINE_BREAK_TOKEN); Scenario scenario = null; for (int i = 0; i < scenarioTokens.length; i++) { String scenarioToken = scenarioTokens[i]; if (RegularExpressionUtil.matches(BehaveConfig.getParser_IdentificationScenarioPattern(), scenarioToken.trim())) { scenario = createScenario(scenarioToken); if (scenario.getReusable()) { scenarios.add(scenario); } else { // Verifica se existe algum filtro informado if (hasFilter(ScenarioFilter.class)) { if (matchesStoryOrScenario(scenario.getIdentification())) { scenarios.add(scenario); } } else { scenarios.add(scenario); } } } else { if (scenario != null) { scenario.getSentences().add(scenarioToken); } } } return scenarios; } /** * Verifica se existe algum filtro do tipo ScenarioFilter ou StoryFilter * * @param c * ScenarioFilter.class ou StoryFilter.class * @return true ou false, se existe ou não existe */ private static boolean hasFilter(Class<?> c) { StoryOrScenarioFilter filter = BehaveContext.getInstance().getStoryOrScenarioFilter(); return filter != null && filter.getClass().getName().equals(c.getName()); } /** * Verifica se o nome do cenário ou história é válido segundo a expressão * regular informada no Filter (ScenarioFilter ou StoryFilter) * * @param value * nome do cenário ou história * @return true ou false, se é valido ou não segundo a expressão regular do * filtro */ private static boolean matchesStoryOrScenario(String value) { String filter = BehaveContext.getInstance().getStoryOrScenarioFilter().getValue(); boolean result = Pattern.compile(filter).matcher(value).find(); return result; } private static Scenario createScenario(String scenarioToken) { String scenarioIdentification = RegularExpressionUtil.getGroup(BehaveConfig.getParser_IdentificationScenarioPattern(), scenarioToken, 3).trim(); String scenarioIdentificationWithoutParametersName = ScenarioParameter.removeParameterNames(scenarioIdentification.toUpperCase()); Scenario scenario = new Scenario(); scenario.setConverted(false); scenario.setDeclaration(scenarioToken); scenario.setIdentification(scenarioIdentification); scenario.setIdentificationWithoutParametersName(scenarioIdentificationWithoutParametersName); scenario.setSentences(new ArrayList<String>()); // Se a identificação do cenário com e sem parâmetros não for igual ele // é um cenário que tem parâmetros, e por tanto é reutilizável if (!scenario.getIdentification().toLowerCase().equals(scenario.getIdentificationWithoutParametersName().toLowerCase())) { scenario.setReusable(true); } return scenario; } private static Map<String, String> scenariosToStories(Map<String, String> storyDefinitions, Map<String, List<Scenario>> scenarios) { Map<String, String> stories = new LinkedHashMap<String, String>(); for (String storyPath : scenarios.keySet()) { String sd = storyDefinitions.get(storyPath); if (hasFilter(StoryFilter.class) || hasFilter(ScenarioFilter.class)) { if (hasFilter(StoryFilter.class) && matchesStoryOrScenario(sd)) { stories.put(storyPath, sd + scenariosToText(scenarios.get(storyPath))); } if (hasFilter(ScenarioFilter.class) && scenarios.get(storyPath).size() > 0) { stories.put(storyPath, sd + scenariosToText(scenarios.get(storyPath))); } } else { stories.put(storyPath, sd + scenariosToText(scenarios.get(storyPath))); } } return stories; } /** * Gera o arquivo processado com as quebras de linhas * * @param scenarios * @return */ private static String scenariosToText(List<Scenario> scenarios) { String text = ""; for (Scenario scenario : scenarios) { if (!scenario.getReusable()) { if (text.length() > 0) { text += LINE_BREAK_TOKEN; } text += scenario.getDeclaration().replaceAll("\t", "") + LINE_BREAK_TOKEN; for (String sentence : scenario.getSentences()) { String s = removeComment(sentence); if (s.trim().length() > 0) text += s.replaceAll("\t", "") + LINE_BREAK_TOKEN; } } } return text; } private static void reuseScenario(Map<String, List<Scenario>> scenarios) { Map<String, Scenario> scenariosIdentificationMap = createScenariosIdentificationMap(scenarios); for (Entry<String, Scenario> entrySet : scenariosIdentificationMap.entrySet()) { reuseScenarioSentences(entrySet.getValue(), entrySet.getValue(), scenariosIdentificationMap); } } private static Map<String, Scenario> createScenariosIdentificationMap(Map<String, List<Scenario>> scenarios) { // Converte todos os cenários de todas as stories em um map // <Identificacao do cenário, cenário> Map<String, Scenario> scenariosIdentificationMap = new LinkedHashMap<String, Scenario>(); for (Entry<String, List<Scenario>> entrySet : scenarios.entrySet()) { for (Scenario scenario : entrySet.getValue()) { scenariosIdentificationMap.put(scenario.getIdentificationWithoutParametersName(), scenario); } } return scenariosIdentificationMap; } /** * @param topScenario * : utilizado para o tratamento de loop infinito no reuso de * histórias * @param scenario * @param scenariosIdentificationMap */ private static void reuseScenarioSentences(Scenario topScenario, Scenario scenario, Map<String, Scenario> scenariosIdentificationMap) { List<String> sentences = new ArrayList<String>(); for (String sentence : scenario.getSentences()) { // Removida a condição que impedia o reuso de passos negócio dentro // de passos de negócio String sentenceWithoutPrefixAndParametersName = RegularExpressionUtil.getGroup(BehaveConfig.getParser_PrefixesBddPattern(), sentence.trim(), 3); if (sentenceWithoutPrefixAndParametersName == null) { sentenceWithoutPrefixAndParametersName = sentence; } sentenceWithoutPrefixAndParametersName = sentenceWithoutPrefixAndParametersName.trim(); sentenceWithoutPrefixAndParametersName = ScenarioParameter.removeParameterNames(sentenceWithoutPrefixAndParametersName).toUpperCase(); // Tratamento para loop infinito no reuso de histórias if (sentenceWithoutPrefixAndParametersName.equals(topScenario.getIdentificationWithoutParametersName())) { throw new BehaveException(bm.getString("exception-scenario-cyclic-reference", topScenario.getIdentification())); } if (scenariosIdentificationMap.containsKey(sentenceWithoutPrefixAndParametersName)) { // A sentença é na verdade uma referência a outro cenário Scenario scenarioReused = scenariosIdentificationMap.get(sentenceWithoutPrefixAndParametersName); if (!scenarioReused.getConverted()) { // Foi utilizada recursão pois é possível que um cenário // chame outro cenário que chame outro cenário reuseScenarioSentences(topScenario, scenarioReused, scenariosIdentificationMap); } List<String> sentencesReplacedCallParameters = ScenarioParameter.replaceCallParameters(sentence.trim(), scenarioReused); sentences.addAll(sentencesReplacedCallParameters); } else { sentences.add(sentence); } } scenario.setConverted(true); scenario.setSentences(sentences); } }