MediaQueryUtil.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.mediaquery;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.WebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.wcm.qa.glnm.configuration.GaleniumConfiguration;
import io.wcm.qa.glnm.configuration.PropertiesUtil;
import io.wcm.qa.glnm.context.GaleniumContext;
import io.wcm.qa.glnm.exceptions.GaleniumException;

/**
 * Some convenience methods around  {@link io.wcm.qa.glnm.mediaquery.MediaQuery}.
 *
 * @since 1.0.0
 */
public final class MediaQueryUtil {

  private static final int CONFIGURED_MAX_WIDTH = GaleniumConfiguration.getMediaQueryMaximalWidth();
  private static final int CONFIGURED_MIN_WIDTH = GaleniumConfiguration.getMediaQueryMinimalWidth();
  private static final MediaQuery DEFAULT_MEDIA_QUERY = getNewMediaQuery("DEFAULT_MQ", CONFIGURED_MIN_WIDTH, CONFIGURED_MAX_WIDTH);

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

  private static final Map<String, Collection<MediaQuery>> MAP_MEDIA_QUERIES_FILENAMES = new HashMap<>();
  private static final Map<Properties, Collection<MediaQuery>> MAP_MEDIA_QUERIES_PROPERTIES = new HashMap<>();

  private MediaQueryUtil() {
    // do not instantiate
  }

  /**
   * <p>
   * MQ for viewport width.
   * </p>
   *
   * @param width viewport
   * @return the media query used in current test device
   * @since 5.0.0
   */
  public static MediaQuery getMediaQueryForViewportWidth(int width) {
    WebDriver driver = GaleniumContext.getDriver();
    if (driver == null) {
      return DEFAULT_MEDIA_QUERY;
    }
    return getMatchingMediaQuery(width);
  }

  /**
   * Returns matching media query from configuration.
   *
   * @param width to match
   * @return media query
   * @since 3.0.0
   */
  public static MediaQuery getMatchingMediaQuery(int width) {
    Collection<MediaQuery> mediaQueries = getMediaQueries();
    for (MediaQuery mediaQuery : mediaQueries) {
      if (matchesViewportWidth(mediaQuery, width)) {
        return mediaQuery;
      }
    }
    return DEFAULT_MEDIA_QUERY;
  }

  /**
   * <p>getMediaQueries.</p>
   *
   * @return get all defined media queries
   * @since 3.0.0
   */
  public static Collection<MediaQuery> getMediaQueries() {
    String propertiesFilePath = GaleniumConfiguration.getMediaQueryPropertiesPath();
    if (StringUtils.isBlank(propertiesFilePath)) {
      throw new GaleniumException("path to media query properties is empty");
    }
    Collection<MediaQuery> mediaQueries = getMediaQueries(propertiesFilePath);
    return mediaQueries;
  }

  /**
   * <p>getMediaQueries.</p>
   *
   * @param mediaQueryPropertyFile to get media query definitions from
   * @return all media queries configured in properties file
   * @since 3.0.0
   */
  public static Collection<MediaQuery> getMediaQueries(File mediaQueryPropertyFile) {
    if (mediaQueryPropertyFile == null) {
      throw new GaleniumException("media query properties file is null.");
    }
    if (!mediaQueryPropertyFile.isFile()) {
      throw new GaleniumException("media query properties file is not a file.");
    }
    return getMediaQueries(mediaQueryPropertyFile.getPath());
  }

  /**
   * <p>getMediaQueries.</p>
   *
   * @param mediaQueryProperties to get media query definitions from
   * @return all media queries configured in properties
   * @since 3.0.0
   */
  public static Collection<MediaQuery> getMediaQueries(Properties mediaQueryProperties) {
    if (MAP_MEDIA_QUERIES_PROPERTIES.containsKey(mediaQueryProperties)) {
      return MAP_MEDIA_QUERIES_PROPERTIES.get(mediaQueryProperties);
    }
    Collection<MediaQuery> mediaQueries = new ArrayList<MediaQuery>();
    SortedMap<Integer, String> sortedMediaQueryMap = getSortedMediaQueryMap(mediaQueryProperties);
    Set<Entry<Integer, String>> entrySet = sortedMediaQueryMap.entrySet();
    int lowerBound = CONFIGURED_MIN_WIDTH;
    for (Entry<Integer, String> entry : entrySet) {
      String mediaQueryName = entry.getValue();
      int upperBound = entry.getKey();
      mediaQueries.add(getNewMediaQuery(mediaQueryName, lowerBound, upperBound));
      lowerBound = upperBound + 1;
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("generated " + mediaQueries.size() + " media queries");
      for (MediaQuery mediaQuery : mediaQueries) {
        LOG.debug("  " + mediaQuery);
      }
    }
    MAP_MEDIA_QUERIES_PROPERTIES.put(mediaQueryProperties, mediaQueries);
    return mediaQueries;
  }


  /**
   * <p>getMediaQueries.</p>
   *
   * @param propertiesPath to get properties file or resource with media query definitions from
   * @return all media queries configured in properties file
   * @since 3.0.0
   */
  public static Collection<MediaQuery> getMediaQueries(String propertiesPath) {
    if (MAP_MEDIA_QUERIES_FILENAMES.containsKey(propertiesPath)) {
      return MAP_MEDIA_QUERIES_FILENAMES.get(propertiesPath);
    }
    Properties mediaQueryProperties = PropertiesUtil.loadProperties(propertiesPath);
    Collection<MediaQuery> mediaQueries = getMediaQueries(mediaQueryProperties);
    MAP_MEDIA_QUERIES_FILENAMES.put(propertiesPath, mediaQueries);
    return mediaQueries;
  }

  /**
   * <p>getMediaQueryByName.</p>
   *
   * @param name of media query to retrieve
   * @return media query matching name
   * @since 3.0.0
   */
  public static MediaQuery getMediaQueryByName(String name) {
    Collection<MediaQuery> mediaQueries = getMediaQueries();
    for (MediaQuery mediaQuery : mediaQueries) {
      if (StringUtils.equals(name, mediaQuery.getName())) {
        return mediaQuery;
      }
    }
    throw new GaleniumException("did not find media query for '" + name + "'");
  }

  /**
   * Factory method to create  {@link io.wcm.qa.glnm.mediaquery.MediaQuery}
   *
   * @param mediaQueryName name to use for media query
   * @param lowerBound to use as lower bound in media query
   * @param upperBound to use as upper bound in media query
   * @return a new media query instance
   * @since 3.0.0
   */
  public static MediaQuery getNewMediaQuery(String mediaQueryName, int lowerBound, int upperBound) {
    if (lowerBound < CONFIGURED_MIN_WIDTH) {
      throw new GaleniumException("MediaQuery: illegally low lower bound for '" + mediaQueryName + "': " + lowerBound + " < " + CONFIGURED_MIN_WIDTH);
    }
    if (upperBound < CONFIGURED_MIN_WIDTH) {
      throw new GaleniumException("MediaQuery: illegally low upper bound for '" + mediaQueryName + "': " + upperBound + " < " + CONFIGURED_MIN_WIDTH);
    }
    if (upperBound > CONFIGURED_MAX_WIDTH) {
      throw new GaleniumException("MediaQuery: illegally high upper bound for '" + mediaQueryName + "': " + upperBound + " > " + CONFIGURED_MAX_WIDTH);
    }
    if (lowerBound > upperBound) {
      throw new GaleniumException("illegal media query lower and upper bound combination for '" + mediaQueryName + "': " + lowerBound + " > " + upperBound);
    }
    return new MediaQueryInstance(mediaQueryName, lowerBound, upperBound);
  }

  private static Integer getIntegerValue(Entry<Object, Object> entry) {
    Object value = entry.getValue();
    if (value == null) {
      throw new GaleniumException("value null for '" + entry.getKey() + "'");
    }
    try {
      return Integer.parseInt(value.toString());
    }
    catch (NumberFormatException ex) {
      throw new GaleniumException("could not parse to integer: '" + value, ex);
    }
  }

  private static SortedMap<Integer, String> getSortedMediaQueryMap(Properties mediaQueryProperties) {
    SortedMap<Integer, String> mediaQueryMap = new TreeMap<Integer, String>();
    Set<Entry<Object, Object>> mqEntries = mediaQueryProperties.entrySet();
    for (Entry<Object, Object> entry : mqEntries) {
      Integer intValue = getIntegerValue(entry);
      String mediaQueryName = entry.getKey().toString();
      mediaQueryMap.put(intValue, mediaQueryName);
    }
    return mediaQueryMap;
  }

  private static boolean matchesViewportWidth(MediaQuery mediaQuery, int width) {
    if (width < mediaQuery.getLowerBound()) {
      return false;
    }
    if (width > mediaQuery.getUpperBound()) {
      return false;
    }
    return true;
  }

  private static final class MediaQueryInstance implements MediaQuery {

    private int high;
    private int low;
    private String name;

    MediaQueryInstance(String mediaQueryName, int lowerBound, int upperBound) {
      name = mediaQueryName;
      low = lowerBound;
      high = upperBound;
    }

    @Override
    public int getLowerBound() {
      return low;
    }

    @Override
    public String getName() {
      return name;
    }

    @Override
    public int getUpperBound() {
      return high;
    }

    @Override
    public String toString() {
      return name + "(lower: " + low + ", upper: " + high + ")";
    }
  }

}