| /******************************************************************************* |
| * Copyright (c) 2006, 2016 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * 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(); |
| } |
| } |
| } |