IcUtil.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2018 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.imagecomparison;

import static io.wcm.qa.glnm.configuration.GaleniumConfiguration.getActualImagesDirectory;
import static io.wcm.qa.glnm.configuration.GaleniumConfiguration.getExpectedImagesDirectory;
import static io.wcm.qa.glnm.util.FileHandlingUtil.constructRelativePath;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;

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

import com.galenframework.parser.SyntaxException;
import com.galenframework.speclang2.specs.SpecReader;
import com.galenframework.specs.Spec;
import com.galenframework.validation.ImageComparison;
import com.galenframework.validation.ValidationError;
import com.galenframework.validation.ValidationResult;

import io.wcm.qa.glnm.exceptions.GaleniumException;
import io.wcm.qa.glnm.selectors.base.Selector;
import io.wcm.qa.glnm.util.FileHandlingUtil;

/**
 * Utility methods for Galenium image comparison via Galen.
 */
final class IcUtil {

  private static final BufferedImage DUMMY_IMAGE = new BufferedImage(20, 20, BufferedImage.TYPE_3BYTE_BGR);

  private static final String DUMMY_IMAGE_FORMAT = "png";

  private static final Logger LOG = LoggerFactory.getLogger(IcUtil.class);

  private static final String REGEX_IMAGE_FILENAME = ".*image file ([^,]*\\.png).*";
  static final Pattern REGEX_PATTERN_IMAGE_FILENAME = Pattern.compile(REGEX_IMAGE_FILENAME);

  private IcUtil() {
    // do not instantiate
  }

  private static String getImageComparisonSpecText(String folder, String fileName, String error, int offset, List<Selector> toIgnore) {
    StringBuilder specText = new StringBuilder()
    // boiler plate
        .append("image file ")
    // image file
        .append(getImageOrDummySamplePath(folder, fileName));

    // tolerance
    if (StringUtils.isNotBlank(error)) {
      specText.append(", error ")
          .append(error);
    }
    if (offset > 0) {
      specText.append(", analyze-offset ");
      specText.append(offset);
    }
    if (!toIgnore.isEmpty()) {
      List<Selector> objects = toIgnore;
      specText.append(", ignore-objects ");
      if (objects.size() == 1) {
        specText.append(objects.get(0));
      }
      else {
        specText.append("[");
        Collection<String> elementNames = new HashSet<String>();

        for (Selector object : objects) {
          elementNames.add(object.elementName());
        }

        specText.append(StringUtils.join(elementNames, ", "));
        specText.append("]");
      }
    }

    return specText.toString();
  }

  private static String getImageOrDummySamplePath(String folder, String fileName) {
    String fullFilePath;

    // folder
    if (StringUtils.isNotBlank(folder)) {
      fullFilePath = FilenameUtils.concat(folder, fileName);
    }
    else {
      // no folder means fileName is all the path info we have
      fullFilePath = fileName;
    }

    createDummyIfSampleDoesNotExist(fullFilePath);

    return fullFilePath;
  }

  private static File getOriginalFilteredImage(ValidationResult result) {
    ImageComparison imageComparison = getImageComparison(result);
    if (imageComparison == null) {
      return null;
    }
    File actualImage = imageComparison.getOriginalFilteredImage();
    if (actualImage == null) {
      LOG.debug("could not find sampled image in image comparison.");
    }

    return actualImage;
  }

  private static File getSampleTargetFile(Spec spec) {
    String targetPath = getTargetPathFrom(spec);
    File imageFile = new File(targetPath);
    FileHandlingUtil.ensureParent(imageFile);
    return imageFile;
  }

  private static String getTargetPathFrom(Spec spec) {
    File rootDirectory = new File(getExpectedImagesDirectory());
    String imagePathFromSpec = getImagePathFrom(spec);
    String relativeImagePath = constructRelativePath(rootDirectory, new File(imagePathFromSpec));
    return getActualImagesDirectory() + File.separator + relativeImagePath;
  }

  private static boolean isExpectedImageSampleMissing(String fullFilePath) {
    return !new File(fullFilePath).isFile();
  }

  private static File writeDummySample(File targetFile) {
    try {
      if (LOG.isTraceEnabled()) {
        LOG.trace("begin writing dummy image '" + targetFile);
      }
      FileHandlingUtil.ensureParent(targetFile);
      if (ImageIO.write(DUMMY_IMAGE, DUMMY_IMAGE_FORMAT, targetFile)) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("done writing dummy image '" + targetFile);
        }
      }
      else if (LOG.isInfoEnabled()) {
        LOG.info("could not write dummy image '" + targetFile);
      }
      return targetFile;
    }
    catch (IOException ex) {
      throw new GaleniumException("could not write dummy image.", ex);
    }
  }

  static void createDummyIfSampleDoesNotExist(String fullFilePath) {
    if (IcUtil.isExpectedImageSampleMissing(fullFilePath)) {
      if (LOG.isInfoEnabled()) {
        LOG.info("Cannot find sample. Substituting dummy for '" + fullFilePath + "'");
      }

      // if image is missing, we'll substitute a dummy to force Galen to at least sample the page
      File targetFile = new File(fullFilePath);

      writeDummySample(targetFile);
    }
  }

  static ImageComparison getImageComparison(ValidationResult result) {
    ValidationError error = result.getError();
    if (error == null) {
      LOG.debug("could not find error in validation result.");
      return null;
    }

    ImageComparison imageComparison = error.getImageComparison();
    if (imageComparison == null) {
      LOG.debug("could not find image comparison in validation error.");
      return null;
    }

    return imageComparison;
  }

  static String getImageComparisonSpecText(IcsDefinition def) {
    return IcUtil.getImageComparisonSpecText(
        def.getFoldername(),
        def.getFilename(),
        def.getAllowedError(),
        def.getAllowedOffset(),
        def.getObjectsToIgnore());
  }

  static String getImagePathFrom(Spec spec) {
    Matcher matcher = REGEX_PATTERN_IMAGE_FILENAME.matcher(spec.toText());
    if (matcher.matches() && matcher.groupCount() >= 1) {
      return matcher.group(1);
    }
    return "";
  }

  static File getSampleSourceFile(Spec spec, ValidationResult result) {
    File imageFile = getOriginalFilteredImage(result);
    if (imageFile != null) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("sample source file: " + imageFile.getPath());
      }
      return imageFile;
    }
    String imagePath = getImagePathFrom(spec);
    if (StringUtils.isBlank(imagePath)) {
      if (LOG.isWarnEnabled()) {
        LOG.warn("could not extract image name from: " + spec.toText());
      }
      return null;
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("sample source path: " + imagePath);
    }
    return new File(imagePath);
  }

  static Spec getSpecForText(String specText) {
    try {
      return new SpecReader().read(specText);
    }
    catch (IllegalArgumentException | SyntaxException ex) {
      String msg = "when parsing spec text: '" + specText + "'";
      LOG.error(msg);
      throw new GaleniumException(msg, ex);
    }
  }

  static String getZeroToleranceImageComparisonSpecText(IcsDefinition def) {
    return getImageComparisonSpecText(
        def.getFoldername(),
        def.getFilename(),
        "",
        0,
        def.getObjectsToIgnore());
  }

  static boolean isImageComparisonSpec(Spec spec) {
    return StringUtils.contains(spec.toText(), "image file ");
  }

  static void saveSample(String objectName, Spec spec, ValidationResult result) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("checking for image file: " + spec.toText() + " (with regex: " + REGEX_PATTERN_IMAGE_FILENAME.pattern() + ")");
    }
    File source = getSampleSourceFile(spec, result);
    if (source == null) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("did not find source file: " + objectName);
      }
      return;
    }

    File target = getSampleTargetFile(spec);
    if (LOG.isTraceEnabled()) {
      LOG.trace("begin copying image '" + source + "' -> '" + target + "'");
    }
    try {
      FileUtils.copyFile(source, target);
    }
    catch (GaleniumException | IOException ex) {
      String msg = "could not write image: " + target;
      LOG.error(msg, ex);
    }
    if (LOG.isTraceEnabled()) {
      LOG.trace("done copying image '" + source + "' -> '" + target + "'");
    }
  }

}