001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2024 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 012 * specific language governing permissions and limitations under the License. 013 */ 014package org.microbean.bean; 015 016import java.util.ArrayList; 017import java.util.List; 018 019import java.util.function.BiFunction; 020 021import static java.util.Collections.unmodifiableList; 022 023import static org.microbean.bean.Ranked.DEFAULT_RANK; 024 025/** 026 * A {@link Reducer} implementation that works with {@link Ranked} objects. 027 * 028 * @param <C> the type of criteria 029 * 030 * @param <T> a {@link Ranked} type 031 * 032 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 033 * 034 * @see #reduce(List, Object, BiFunction) 035 */ 036public final class RankedReducer<C, T extends Ranked> implements Reducer<C, T> { 037 038 039 /* 040 * Static fields. 041 */ 042 043 044 private static final RankedReducer<?, ?> INSTANCE = new RankedReducer<>(); 045 046 047 /* 048 * Constructors. 049 */ 050 051 052 private RankedReducer() { 053 super(); 054 } 055 056 057 /* 058 * Instance methods. 059 */ 060 061 062 @Override 063 public final T reduce(final List<? extends T> elements, 064 final C c, 065 final BiFunction<? super List<? extends T>, ? super C, ? extends T> failureHandler) { 066 if (elements == null || elements.isEmpty()) { 067 return null; 068 } else if (elements.size() == 1) { 069 return elements.get(0); 070 } 071 072 T candidate = null; 073 List<T> unresolved = null; 074 // Highest rank wins 075 int maxRank = DEFAULT_RANK; 076 077 for (final T element : elements) { 078 if (element.alternate()) { 079 final int elementRank = element.rank(); 080 if (elementRank == maxRank) { 081 if (candidate == null || !candidate.alternate()) { 082 // Prefer elements regardless of ranks. 083 candidate = element; 084 } else { 085 assert candidate.rank() == maxRank : "Unexpected rank: " + candidate.rank() + "; was expecting: " + maxRank; 086 // The existing candidate is an alternate and by definition has the highest rank we've seen so far; the 087 // incoming element is also an alternate; both have equal ranks: we can't resolve this. 088 if (unresolved == null) { 089 unresolved = new ArrayList<>(6); 090 } 091 unresolved.add(candidate); 092 unresolved.add(element); 093 candidate = null; 094 } 095 } else if (elementRank > maxRank) { 096 if (candidate == null || !candidate.alternate() || elementRank > candidate.rank()) { 097 // The existing candidate is either null, not an alternate (and alternates are always preferred), or an 098 // alternate with losing rank, so junk it in favor of the incoming element. 099 candidate = element; 100 // We have a new maxRank. 101 maxRank = elementRank; 102 } else if (elementRank == candidate.rank()) { 103 // The existing candidate is also an alternate and has the same rank. 104 if (unresolved == null) { 105 unresolved = new ArrayList<>(6); 106 } 107 unresolved.add(candidate); 108 unresolved.add(element); 109 candidate = null; 110 } else { 111 assert elementRank < candidate.rank() : "elementRank >= candidate.rank(): " + elementRank + " >= " + candidate.rank(); 112 // The existing candidate is also an alternate but has a higher rank than the alternate, so keep it (do 113 // nothing). 114 } 115 } 116 // ...else drop element by doing nothing 117 } else if (candidate == null) { 118 // The incoming element is not an alternate, but that doesn't matter; the candidate is null, so accept the 119 // element no matter what. 120 candidate = element; 121 } else if (!candidate.alternate()) { 122 // The existing candidate is not an alternate. The incoming element is not an alternate. Ranks in this case are 123 // irrelevant, perhaps surprisingly. We cannot resolve this. 124 if (unresolved == null) { 125 unresolved = new ArrayList<>(6); 126 } 127 unresolved.add(candidate); 128 unresolved.add(element); 129 candidate = null; 130 } 131 // ...else do nothing 132 } 133 134 if (unresolved != null && !unresolved.isEmpty()) { 135 if (candidate != null) { 136 unresolved.add(candidate); 137 } 138 candidate = 139 failureHandler == null ? Reducer.fail(unmodifiableList(unresolved), c) : failureHandler.apply(unmodifiableList(unresolved), c); 140 } 141 142 return candidate; 143 } 144 145 146 /* 147 * Static methods. 148 */ 149 150 151 /** 152 * Returns a {@link RankedReducer} implementation. 153 * 154 * @param <C> the type of criteria 155 * 156 * @param <T> the type of the {@link Ranked} reduction 157 * 158 * @return a {@link RankedReducer} implementation; never {@code null} 159 * 160 * @nullability This method never returns {@code null}. 161 * 162 * @idempotency This method is idempotent and deterministic. 163 * 164 * @threadsafety This method is safe for concurrent use by multiple threads. 165 */ 166 @SuppressWarnings("unchecked") 167 public static final <C, T extends Ranked> Reducer<C, T> of() { 168 return (Reducer<C, T>)INSTANCE; 169 } 170 171}