org.ambraproject.wombat.config.site.SiteSet.java Source code

Java tutorial

Introduction

Here is the source code for org.ambraproject.wombat.config.site.SiteSet.java

Source

/*
 * Copyright (c) 2017 Public Library of Science
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

package org.ambraproject.wombat.config.site;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import org.ambraproject.wombat.config.RuntimeConfigurationException;
import org.ambraproject.wombat.config.site.url.SiteRequestScheme;
import org.ambraproject.wombat.config.theme.Theme;
import org.ambraproject.wombat.config.theme.ThemeGraph;
import org.ambraproject.wombat.service.UnmatchedSiteException;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Simple wrapper around a map from keys to site objects, for use as a Spring bean.
 */
public class SiteSet {

    private final ImmutableBiMap<String, Site> sites;

    private final ImmutableBiMap<String, String> journalKeysToNames;

    @VisibleForTesting // otherwise use SiteSet.create
    public SiteSet(Iterable<Site> sites) {
        ImmutableBiMap.Builder<String, Site> map = ImmutableBiMap.builder();
        for (Site site : sites) {
            map.put(site.getKey(), site);
        }
        this.sites = map.build();

        this.journalKeysToNames = buildJournalKeysToNames(this.sites.values());
    }

    private static ImmutableMultimap<String, Site> groupByJournalKey(Set<Site> sites) {
        ImmutableMultimap.Builder<String, Site> builder = ImmutableMultimap.builder();
        for (Site site : sites) {
            builder.put(site.getJournalKey(), site);
        }
        return builder.build();
    }

    /**
     * Build a map representing the one-to-one relationship between journal keys and journal names.
     * <p/>
     * As a side effect, validates that the relationship actually is one-to-one -- that is, that multiple sites have the
     * same journal key if and only if they have the same journal name. It is easier to obey this constraint if the {@code
     * journalKey} and {@code journalName} config values are always set in the {@code journal.yaml} or {@code
     * journal.json} file of the same theme, which should be a parent of all themes belonging to that journal.
     *
     * @param sites the set of all sites being served
     * @return a map between journal keys and journal names
     * @throws IllegalArgumentException if two sites with the same journal key have unequal journal names or if two sites
     *                                  with the same journal name have unequal journal keys
     */
    private static ImmutableBiMap<String, String> buildJournalKeysToNames(Set<Site> sites) {
        Multimap<String, Site> keysToSites = groupByJournalKey(sites);
        BiMap<String, String> keysToNames = HashBiMap.create(keysToSites.keySet().size());
        for (Map.Entry<String, Collection<Site>> entry : keysToSites.asMap().entrySet()) {
            String journalKey = entry.getKey();
            Iterator<Site> siteIterator = entry.getValue().iterator();
            String journalName = siteIterator.next().getJournalName();
            while (siteIterator.hasNext()) {
                String nextJournalName = siteIterator.next().getJournalName();
                if (!journalName.equals(nextJournalName)) {
                    String message = String.format("Inconsistent journal names with key=%s: %s; %s", journalKey,
                            journalName, nextJournalName);
                    throw new IllegalArgumentException(message);
                }
            }

            if (keysToNames.containsValue(journalName)) {
                String message = String.format("Overloaded journal name (%s) for keys: %s; %s", journalName,
                        journalKey, keysToNames.inverse().get(journalName));
                throw new IllegalArgumentException(message);
            }
            keysToNames.put(journalKey, journalName);
        }
        return ImmutableBiMap.copyOf(keysToNames);
    }

    public static SiteSet create(List<? extends Map<String, ?>> siteSpecifications, ThemeGraph themeGraph) {
        List<Site> sites = Lists.newArrayListWithCapacity(siteSpecifications.size());
        for (Map<String, ?> siteSpec : siteSpecifications) {
            String siteKey = (String) siteSpec.get("key");
            String themeKey = (String) siteSpec.get("theme");
            String siteType = (String) siteSpec.get("type");
            Theme theme = themeGraph.getTheme(themeKey);
            if (theme == null) {
                throw new RuntimeException(
                        String.format("No theme with key=\"%s\" found (for site: %s)", themeKey, siteKey));
            }

            Map<String, ?> resolveDefinition = (Map<String, ?>) siteSpec.get("resolve");
            SiteRequestScheme requestScheme = resolveDefinition != null ? parseRequestScheme(resolveDefinition)
                    : SiteRequestScheme.builder().setPathToken(siteKey).build();

            sites.add(new Site(siteKey, theme, requestScheme, siteType));
        }
        validateSchemes(sites);
        return new SiteSet(sites);
    }

    private static SiteRequestScheme parseRequestScheme(Map<String, ?> resolveDefinition) {
        SiteRequestScheme.Builder builder = SiteRequestScheme.builder();

        String pathToken = (String) resolveDefinition.get("path");
        if (pathToken != null) {
            builder.setPathToken(pathToken);
        }

        List<Map<String, ?>> headers = (List<Map<String, ?>>) resolveDefinition.get("headers");
        if (headers != null && !headers.isEmpty()) {
            for (Map<String, ?> headerSpec : headers) {
                String headerName = (String) headerSpec.get("name");
                String headerValue = (String) headerSpec.get("value");
                if (headerName == null || headerValue == null) {
                    throw new RuntimeConfigurationException(
                            "Each entry under \"headers\" must have a \"name\" and \"value\" field");
                }
                builder.requireHeader(headerName, headerValue);
            }
        }

        String hostName = (String) resolveDefinition.get("host");
        if (hostName != null) {
            builder.specifyHost(hostName);
        }

        return builder.build();
    }

    /**
     * @throws RuntimeConfigurationException if there a detectable collision between site config values
     */
    private static void validateSchemes(Collection<Site> sites) {
        Map<String, Site> keys = Maps.newHashMapWithExpectedSize(sites.size());
        Map<SiteRequestScheme, Site> requestSchemes = Maps.newHashMapWithExpectedSize(sites.size());
        for (Site site : sites) {
            Site previous;

            String key = site.getKey();
            previous = keys.put(key, site);
            if (previous != null) {
                throw new RuntimeConfigurationException("Multiple sites have the same key: " + key);
            }

            /*
             * Depends on the equals and hashCode implementations of the SiteRequestPredicate implementations contained in
             * the SiteRequestScheme. If they don't provide good overrides, this check is still safe against false positives
             * but may not catch all error conditions.
             */
            SiteRequestScheme requestScheme = site.getRequestScheme();
            previous = requestSchemes.put(requestScheme, site);
            if (previous != null) {
                String message = String.format("Multiple sites (%s, %s) have the same request scheme: %s", key,
                        previous.getKey(), requestScheme);
                throw new RuntimeConfigurationException(message);
            }
        }
    }

    /**
     * Attempts to load a site based on site key.
     *
     * @param key specifies the site
     * @return Site instance matching the key
     * @throws UnmatchedSiteException if no site is found
     */
    public Site getSite(String key) throws UnmatchedSiteException {
        Site site = sites.get(key);
        if (site == null) {
            throw new UnmatchedSiteException("Key not matched to site: " + key);
        }
        return site;
    }

    public ImmutableSet<Site> getSites() {
        return sites.values();
    }

    /**
     * @return a journal key for a requested journal name
     */
    public String getJournalKeyFromName(String journalName) {
        String journalKey = journalKeysToNames.inverse().get(journalName);
        if (journalKey == null) {
            throw new UnmatchedSiteException("Journal name not matched to key: " + journalName);
        }
        return journalKey;
    }

    public String getJournalNameFromKey(String journalKey) {
        String journalName = journalKeysToNames.get(journalKey);
        if (journalName == null) {
            throw new UnmatchedSiteException("Journal key not matched to name: " + journalKey);
        }
        return journalName;
    }

    public ImmutableSet<String> getSiteKeys() {
        return sites.keySet();
    }

    /**
     * @return a set of all journal keys for this SiteSet.  Note that there may be fewer of
     *     these than siteKeys, since a journal can have multiple sites.
     */
    public ImmutableSet<String> getJournalKeys() {
        return journalKeysToNames.keySet();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        return sites.equals(((SiteSet) o).sites);
    }

    @Override
    public int hashCode() {
        return sites.hashCode();
    }

}