/******************************************************************************* * Copyright (c) 2006, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.help.internal.util; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Set; /* * Provides an algorithm for determining a recommended sequence of items that * satisfies a primary sequence and as many secondary sequences as possible. * * For example, this is used to determine the display order of books on the tocs * based on the active product's preferred order as well as all other products' * preferred orders. */ public class SequenceResolver<T> { private List<T> primaryList; private List<T>[] secondaryLists; private ListIterator<T> primaryIter; private ListIterator<T>[] secondaryIters; private Set<T> processedItems; /* * Merges the given primary and secondary orderings such that all ordering * conditions from the primary are satisfied, and as many secondary ordering * conditions as reasonably possible are also satisfied. An ordering condition * is a pair of adjacent items in any ordering. * * For example: * * primary = {b, d, f} * secondary[0] = {c, d, e} * secondary[1] = {a, b, c} * ------------------------------- * result = {a, b, c, d, e, f} * * The algorithm works in iterations, where at each iteration we determine * the next element in the recommended sequence. We maintain a pointer for * each ordering to keep track of what we've already ordered. * * To determine the next item, we locate the current element in each ordering. * These are the candidates for the next item as the next item can only be * one of these. * * The top candidates are selected from the list of candidates, where a top * candidate is one that has the lowest rank (there can be many). Rank is * determined by how many other candidates appear before that candidate * item in all the orderings. That is, we find out how many items * the orderings list before each candidate. * * If a candidate has no other candidates listed before it, it will be * the next item. If there are many, the first one is selected. If one of * the top candidates is from the primary ordering (can only be one), it is * automatically selected. * * Using the top rank ensures that if there are conflicts, and that * as many orderings as possible are satisfied. For example, if most orderings * want x before y, but a few want the opposite, x will be placed before y. */ public List<T> getSequence(List<T> primary, List<T>[] secondary) { primaryList = primary; secondaryLists = secondary; prepareDataStructures(); List<T> order = new ArrayList<>(); T item; while ((item = getNextItem()) != null) { processedItems.add(item); advanceIterator(primaryIter); for (int i=0;i<secondaryIters.length;++i) { advanceIterator(secondaryIters[i]); } order.add(item); } return order; } /* * Create the data structures necessary for later operations. */ @SuppressWarnings("unchecked") private void prepareDataStructures() { primaryIter = primaryList.listIterator(); secondaryIters = new ListIterator[secondaryLists.length]; for (int i=0;i<secondaryLists.length;++i) { secondaryIters[i] = secondaryLists[i].listIterator(); } processedItems = new HashSet<>(); } /* * Determine the next item in the sequence based on the top * candidate items. */ private T getNextItem() { Candidate<T>[] candidates = getTopCandidates(); switch(candidates.length) { case 0: return null; case 1: return candidates[0].item; default: for (int i=0;i<candidates.length;++i) { if (candidates[i].isPrimary) { return candidates[i].item; } } return candidates[0].item; } } /* * Retrieves the top candidates from all the available next item candidates. * These are the candidates that have the lowest rank */ private Candidate<T>[] getTopCandidates() { Candidate<T>[] candidates = getEligibleCandidates(); rankCandidates(candidates); if (candidates.length > 0) { int topRank = candidates[0].rank; for (int i=1;i<candidates.length;++i) { if (candidates[i].rank < topRank) { topRank = candidates[i].rank; } } List<Candidate<T>> topCandidates = new ArrayList<>(); for (int i=0;i<candidates.length;++i) { if (candidates[i].rank == topRank) { topCandidates.add(candidates[i]); } } return toCandidatesArray(topCandidates); } return candidates; } /* * Returns all eligible candidates. A candidate is eligible if it does not * conflict with the primary candidate. That is, if the primary candidate's list * has that candidate after the primary candidate, it is contradicting the primary * sequence and is not eligible. */ private Candidate<T>[] getEligibleCandidates() { Candidate<T>[] allCandidates = getAllCandidates(); Candidate<T> primary = null; for (int i=0;i<allCandidates.length;++i) { if (allCandidates[i].isPrimary) { primary = allCandidates[i]; break; } } // if we have no primary candidate then they're all eligible if (primary != null) { List<Candidate<T>> eligibleCandidates = new ArrayList<>(allCandidates.length); // primary candidate is always eligible eligibleCandidates.add(primary); Set<T> primarySet = Collections.singleton(primary.item); for (int i=0;i<allCandidates.length;++i) { Candidate<T> c = allCandidates[i]; if (c != primary) { // does it contradict the primary sequence? if not, it is eligible if (countPrecedingItems(c.item, primary.src, primarySet) == 0) { eligibleCandidates.add(c); } } } return toCandidatesArray(eligibleCandidates); } return allCandidates; } /* * Retrieve all the candidates for the next item in sequence, with * no duplicates. */ private Candidate<T>[] getAllCandidates() { List<Candidate<T>> candidates = new ArrayList<>(); T item = getNextItem(primaryIter); if (item != null) { Candidate<T> c = new Candidate<>(); c.item = item; c.isPrimary = true; c.src = primaryList; candidates.add(c); } for (int i=0;i<secondaryIters.length;++i) { item = getNextItem(secondaryIters[i]); if (item != null) { Candidate<T> c = new Candidate<>(); c.item = item; c.isPrimary = false; c.src = secondaryLists[i]; if (!candidates.contains(c)) { candidates.add(c); } } } return toCandidatesArray(candidates); } /** Helper function as we cannot create arrays of parameterized types */ @SuppressWarnings("unchecked") private Candidate<T>[] toCandidatesArray(List<Candidate<T>> candidates) { return candidates.toArray(new Candidate[candidates.size()]); } /* * Assign a rank to each of the given candidates. Rank is determined by * how many preceding candidates appear before that candidate in the orderings. * This essentially means how far back this item should be in the final * sequence. */ private void rankCandidates(Candidate<T>[] candidates) { // for quick lookup Set<T> candidateItems = new HashSet<>(); for (int i=0;i<candidates.length;++i) { candidateItems.add(candidates[i].item); } for (int i=0;i<candidates.length;++i) { Candidate<T> c = candidates[i]; for (int j=0;j<candidates.length;++j) { c.rank += countPrecedingItems(c.item, candidates[j].src, candidateItems); } } } /* * Counts the number of elements from the given set that come before * the given item in the given list. */ private int countPrecedingItems(Object item, List<?> list, Set<?> set) { int count = 0; Iterator<?> iter = list.iterator(); while (iter.hasNext()) { Object next = iter.next(); if (next.equals(item)) { return count; } if (set.contains(next)) { ++count; } } return 0; } /* * Returns the next item available to this iterator, without moving it * forward. */ private T getNextItem(ListIterator<T> iter) { if (iter.hasNext()) { T next = iter.next(); iter.previous(); return next; } return null; } /* * Advances the given iterator to the next item in its * sequence that we haven't yet processed. */ private void advanceIterator(ListIterator<T> iter) { while (iter.hasNext()) { T item = iter.next(); if (!processedItems.contains(item)) { iter.previous(); break; } } } /* * A candidate item; one that could potentially be the next one in the final * sequence. */ private static class Candidate<T> { public T item; public boolean isPrimary; public int rank; public List<?> src; @Override public boolean equals(Object obj) { return item.equals(obj); } @Override public int hashCode() { return item.hashCode(); } } }