GalenParsing.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2019 wcm.io
 * %%
 * 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.
 * #L%
 */
package io.wcm.qa.glnm.galen.specs;

import static java.util.stream.Collectors.toList;
import static org.apache.commons.collections4.ListUtils.emptyIfNull;
import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
import static org.apache.commons.io.IOUtils.toInputStream;
import static org.apache.commons.lang3.RegExUtils.replacePattern;
import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.StringUtils.removeEnd;
import static org.apache.commons.lang3.StringUtils.startsWith;
import static org.apache.commons.lang3.StringUtils.trim;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.galenframework.parser.SyntaxException;
import com.galenframework.speclang2.pagespec.PageSpecReader;
import com.galenframework.speclang2.pagespec.SectionFilter;
import com.galenframework.specs.page.PageSpec;
import com.galenframework.utils.GalenUtils;
import com.google.common.collect.Lists;

import io.wcm.qa.glnm.configuration.GaleniumConfiguration;
import io.wcm.qa.glnm.exceptions.GaleniumException;
import io.wcm.qa.glnm.galen.mock.MockPage;

/**
 * Helper methods to parse specs from files or strings.
 *
 * @since 4.0.0
 */
final class GalenParsing {

  // Galen uses deprecated method to read with JVM default charset. Using same behavior here.
  private static final Charset GALEN_PARSING_CHARSET = Charset.defaultCharset();
  private static final Map<String, Object> EMPTY_JS_VARS = null;
  private static final Properties EMPTY_PROPERTIES = new Properties();
  private static final Logger LOG = LoggerFactory.getLogger(GalenParsing.class);

  private GalenParsing() {
    // do not instantiate
  }

  private static List<String> getSourceFromResource(String specPath) {
    try {
      InputStream resource = getStream(specPath);
      if (resource == null) {
        if (LOG.isTraceEnabled()) {
          LOG.trace("no stream found when fetching: " + specPath);
        }
        return null;
      }
      List<String> lines = IOUtils.readLines(resource, GALEN_PARSING_CHARSET);
      return rewriteImports(lines, specPath);
    }
    catch (IOException | IllegalArgumentException ex) {
      if (LOG.isTraceEnabled()) {
        LOG.trace("when fetching: " + specPath, ex);
      }
      return null;
    }
  }

  private static List<String> rewriteImports(List<String> lines, String specPath) {
    return emptyIfNull(lines)
        .stream()
        .map(s -> rewriteImport(s, specPath))
        .collect(toList());
  }

  private static String rewriteImport(String inputLine, String importingSpecPath) {
    String trimmedInputLine = trim(inputLine);
    if (startsWith(trimmedInputLine, "@import ")) {
      String importedPath = StringUtils.removeStart(trimmedInputLine, "@import ");
      if (LOG.isDebugEnabled()) {
        LOG.debug("rewriting import: " + inputLine);
        LOG.debug("imported spec path: " + importedPath);
        LOG.debug("importing spec path: " + importingSpecPath);
      }
      String importingFolder = FilenameUtils.getFullPath(importingSpecPath);
      String rewrittenImportedPath = separatorsToUnix(combine(importingFolder, importedPath));
      if (LOG.isDebugEnabled()) {
        LOG.debug("rewritten imported spec path: " + rewrittenImportedPath);
      }
      String rewrittenLine = replacePattern(
          inputLine,
          "@import .*$",
          "@import " + rewrittenImportedPath);
      if (LOG.isDebugEnabled()) {
        LOG.debug("rewritten import: " + rewrittenLine);
      }
      return rewrittenLine;
    }
    return inputLine;
  }

  private static String prependSpecFolder(String specPath) {
    String specFolder = GaleniumConfiguration.getGalenSpecPath();
    String relativePath = StringUtils.removeStart(specPath, "/");
    return combine(specFolder, relativePath);
  }

  private static String combine(String specFolder, String relativePath) {
    String combined = removeEnd(specFolder, "/") + "/" + relativePath;
    String normalized = FilenameUtils.normalize(combined, true);
    if (LOG.isDebugEnabled()) {
      LOG.debug("combining: '" + specFolder + "' + '" + relativePath + "' -> '" + normalized + "'");
    }
    return normalized;
  }

  /**
   * Convenience method to read a Galen spec using current threads context. Basically a convenience mapping to
   * {@link com.galenframework.speclang2.pagespec.PageSpecReader#read(String, com.galenframework.page.Page, SectionFilter, Properties, Map, Map)}.
   * @param specPath path to spec file
   * @param tags include tags to use with spec
   * @return Galen page spec object
   * @since 4.0.0
   */
  static PageSpec fromPath(String specPath, String... tags) {
    try {
      String source = getSource(specPath);
      if (source == null) {
        throw new GaleniumException("Could not find spec at '" + specPath + "'");
      }
      if (StringUtils.isBlank(source)) {
        throw new GaleniumException("Found empty spec at '" + specPath + "'");
      }
      InputStream stream = toInputStream(source, GALEN_PARSING_CHARSET);
      //      String contextPath = FilenameUtils.getPath(specPath);
      SectionFilter filter = getFilter(tags);
      return new PageSpecReader().read(stream, source, null, new MockPage(), filter, EMPTY_PROPERTIES, EMPTY_JS_VARS, null);
    }
    catch (IOException | SyntaxException ex) {
      throw new GaleniumException("Exception when parsing spec: '" + specPath + "'", ex);
    }
  }

  private static SectionFilter getFilter(String... tags) {
    SectionFilter filter = GalenSpecUtil.getDefaultIncludeTags();
    if (ArrayUtils.isNotEmpty(tags)) {
      filter.getIncludedTags().addAll(Lists.newArrayList(tags));
    }
    return filter;
  }

  static String getSource(String specPath) {
    return join(getSourceLines(specPath), "\n");
  }

  static List<String> getSourceLines(String specPath) {
    List<String> source = getSourceFromResource(specPath);
    if (source != null && !source.isEmpty()) {
      return source;
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("did not find at path: '" + specPath + "'");
    }
    String withSpecFolder = prependSpecFolder(specPath);
    if (LOG.isDebugEnabled()) {
      LOG.debug("trying with prepended path: '" + withSpecFolder + "'");
    }
    return getSourceFromResource(withSpecFolder);
  }

  static InputStream getStream(String specPath) {
    return GalenUtils.findFileOrResourceAsStream(specPath);
  }

}