| 1 | /******************************************************************************* | |
| 2 | * Copyright (C) 2026, Leo Galambos | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * 1. Redistributions of source code must retain the above copyright notice, | |
| 9 | * this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, | |
| 12 | * this list of conditions and the following disclaimer in the documentation | |
| 13 | * and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * 3. Neither the name of the copyright holder nor the names of its contributors | |
| 16 | * may be used to endorse or promote products derived from this software | |
| 17 | * without specific prior written permission. | |
| 18 | * | |
| 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
| 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| 29 | * POSSIBILITY OF SUCH DAMAGE. | |
| 30 | ******************************************************************************/ | |
| 31 | package org.egothor.stemmer; | |
| 32 | ||
| 33 | import java.io.DataInputStream; | |
| 34 | import java.io.DataOutputStream; | |
| 35 | import java.io.IOException; | |
| 36 | import java.io.InputStream; | |
| 37 | import java.io.OutputStream; | |
| 38 | import java.nio.CharBuffer; | |
| 39 | import java.nio.charset.StandardCharsets; | |
| 40 | import java.security.MessageDigest; | |
| 41 | import java.security.NoSuchAlgorithmException; | |
| 42 | import java.util.ArrayList; | |
| 43 | import java.util.Arrays; | |
| 44 | import java.util.Collections; | |
| 45 | import java.util.IdentityHashMap; | |
| 46 | import java.util.LinkedHashMap; | |
| 47 | import java.util.List; | |
| 48 | import java.util.Locale; | |
| 49 | import java.util.Map; | |
| 50 | import java.util.Objects; | |
| 51 | import java.util.concurrent.locks.ReentrantLock; | |
| 52 | import java.util.function.IntFunction; | |
| 53 | import java.util.logging.Level; | |
| 54 | import java.util.logging.Logger; | |
| 55 | ||
| 56 | import org.egothor.stemmer.trie.CompiledNode; | |
| 57 | import org.egothor.stemmer.trie.LocalValueSummary; | |
| 58 | import org.egothor.stemmer.trie.MutableNode; | |
| 59 | import org.egothor.stemmer.trie.ReducedNode; | |
| 60 | import org.egothor.stemmer.trie.ReductionContext; | |
| 61 | import org.egothor.stemmer.trie.ReductionSignature; | |
| 62 | ||
| 63 | /** | |
| 64 | * Read-only trie mapping {@link String} keys to one or more values with | |
| 65 | * frequency tracking. | |
| 66 | * | |
| 67 | * <p> | |
| 68 | * A key may be associated with multiple values. Each value keeps the number of | |
| 69 | * times it was inserted during the build phase. The method {@link #get(String)} | |
| 70 | * returns the locally most frequent value stored at the terminal node of the | |
| 71 | * supplied key, while {@link #getAll(String)} returns all locally stored values | |
| 72 | * ordered by descending frequency. | |
| 73 | * | |
| 74 | * <p> | |
| 75 | * If multiple values have the same local frequency, their ordering is | |
| 76 | * deterministic. The preferred value is selected by the following tie-breaking | |
| 77 | * rules, in order: | |
| 78 | * <ol> | |
| 79 | * <li>shorter {@link String} representation wins, based on | |
| 80 | * {@code value.toString()}</li> | |
| 81 | * <li>if the lengths are equal, lexicographically lower {@link String} | |
| 82 | * representation wins</li> | |
| 83 | * <li>if the textual representations are still equal, first-seen insertion | |
| 84 | * order remains stable</li> | |
| 85 | * </ol> | |
| 86 | * | |
| 87 | * <p> | |
| 88 | * Values may be stored at any trie node, including internal nodes and leaf | |
| 89 | * nodes. Therefore, reduction and canonicalization always operate on both the | |
| 90 | * node-local terminal values and the structure of all descendant edges. | |
| 91 | * </p> | |
| 92 | * | |
| 93 | * <p> | |
| 94 | * Instances are immutable and safe for concurrent lookup. A | |
| 95 | * {@linkplain #withLookupMode(LookupMode) lookup-mode view} shares the same | |
| 96 | * compiled node graph and changes only which applicable node or nodes a read | |
| 97 | * selects. Returned arrays and collections are caller-owned or immutable. | |
| 98 | * </p> | |
| 99 | * | |
| 100 | * @param <V> value type | |
| 101 | */ | |
| 102 | @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.CouplingBetweenObjects" }) | |
| 103 | public final class FrequencyTrie<V> { | |
| 104 | ||
| 105 | /** | |
| 106 | * Logger of this class. | |
| 107 | */ | |
| 108 | private static final Logger LOGGER = Logger.getLogger(FrequencyTrie.class.getName()); | |
| 109 | ||
| 110 | /** | |
| 111 | * Domain separator used by the trie fingerprint canonical input. | |
| 112 | */ | |
| 113 | private static final String FINGERPRINT_DOMAIN = "RADIXOR-FREQUENCY-TRIE-FINGERPRINT"; | |
| 114 | ||
| 115 | /** | |
| 116 | * Version of the canonical fingerprint input format. | |
| 117 | */ | |
| 118 | private static final int FINGERPRINT_FORMAT_VERSION = 2; | |
| 119 | ||
| 120 | /** | |
| 121 | * Root node of the compiled read-only trie. | |
| 122 | */ | |
| 123 | private final CompiledNode<V> root; | |
| 124 | ||
| 125 | /** | |
| 126 | * Metadata persisted together with this trie. | |
| 127 | */ | |
| 128 | private final TrieMetadata metadata; | |
| 129 | ||
| 130 | /** | |
| 131 | * Lazily initialized canonical SHA-256 fingerprint bytes. Every read and write | |
| 132 | * is guarded by {@link #fingerprintLock}. The value is assigned only after | |
| 133 | * calculation succeeds, so failed initialization leaves the cache empty. The | |
| 134 | * internal array is never exposed directly to callers. | |
| 135 | */ | |
| 136 | private byte[] fingerprintBytes; | |
| 137 | ||
| 138 | /** | |
| 139 | * Guards all access to {@link #fingerprintBytes}. Holding this lock for every | |
| 140 | * cache read and write safely publishes successful lazy initialization to | |
| 141 | * subsequent callers. | |
| 142 | */ | |
| 143 | private final ReentrantLock fingerprintLock = new ReentrantLock(); | |
| 144 | ||
| 145 | /** | |
| 146 | * Cached traversal direction used for key lookup. | |
| 147 | */ | |
| 148 | private final WordTraversalDirection lookupTraversalDirection; | |
| 149 | ||
| 150 | /** | |
| 151 | * Whether lookups require lowercase normalization. | |
| 152 | */ | |
| 153 | private final boolean lowercasesLookupKeys; | |
| 154 | ||
| 155 | /** | |
| 156 | * Whether lookups require diacritic stripping. | |
| 157 | */ | |
| 158 | private final boolean removeDiacritics; | |
| 159 | ||
| 160 | /** | |
| 161 | * Shared empty array instance for empty lookup results from | |
| 162 | * {@link #getAll(String)}. | |
| 163 | */ | |
| 164 | private final V[] emptyValues; | |
| 165 | ||
| 166 | /** | |
| 167 | * Read-time command-selection policy for the {@code get} / {@code getAll} | |
| 168 | * family. Never persisted; defaults to {@link LookupMode#FIRST}. | |
| 169 | */ | |
| 170 | private final LookupMode lookupMode; | |
| 171 | ||
| 172 | /** | |
| 173 | * Cached {@code true} when {@link #lookupTraversalDirection} consumes keys | |
| 174 | * from the end (BACKWARD), used by the {@code LAST}/{@code ALL} traversals. | |
| 175 | */ | |
| 176 | private final boolean backwardLookup; | |
| 177 | ||
| 178 | /** | |
| 179 | * Binary format magic header. | |
| 180 | */ | |
| 181 | private static final int STREAM_MAGIC = 0x45475452; | |
| 182 | ||
| 183 | /** | |
| 184 | * Minimum supported stream version constant retained for explicit range checks. | |
| 185 | */ | |
| 186 | private static final int MIN_STREAM_VERSION = 1; | |
| 187 | ||
| 188 | /** | |
| 189 | * Number of stored values for which {@link #getEntries(String)} can return an | |
| 190 | * empty result. | |
| 191 | */ | |
| 192 | private static final int NO_VALUE_COUNT = 0; | |
| 193 | ||
| 194 | /** | |
| 195 | * Number of stored values for which {@link #getEntries(String)} can use a | |
| 196 | * one-item immutable list special case. | |
| 197 | */ | |
| 198 | private static final int SINGLE_VALUE_COUNT = 1; | |
| 199 | ||
| 200 | /** | |
| 201 | * Binary format version. | |
| 202 | */ | |
| 203 | private static final int STREAM_VERSION = 7; | |
| 204 | ||
| 205 | /** | |
| 206 | * Version where traversal-direction ordinal is persisted. | |
| 207 | */ | |
| 208 | private static final int TRAVERSAL_VERSION = 2; | |
| 209 | ||
| 210 | /** | |
| 211 | * Version where compact reduction metadata is persisted. | |
| 212 | */ | |
| 213 | private static final int REDUCTION_VERSION = 3; | |
| 214 | ||
| 215 | /** | |
| 216 | * Version where case-processing mode ordinal is persisted. | |
| 217 | */ | |
| 218 | private static final int CASE_VERSION = 4; | |
| 219 | ||
| 220 | /** | |
| 221 | * Version where the persisted metadata switched to a text block. | |
| 222 | */ | |
| 223 | private static final int TEXT_METADATA_VERSION = 5; | |
| 224 | ||
| 225 | /** | |
| 226 | * Version where contracted accepting nodes are persisted. | |
| 227 | */ | |
| 228 | private static final int ACCEPTING_NODE_VERSION = 6; | |
| 229 | ||
| 230 | /** | |
| 231 | * Version where distinct values are persisted once in a stream-local table. | |
| 232 | */ | |
| 233 | private static final int VALUE_TABLE_VERSION = 7; | |
| 234 | ||
| 235 | /** | |
| 236 | * Argument name for lookup keys. | |
| 237 | */ | |
| 238 | private static final String ARG_KEY = "key"; | |
| 239 | ||
| 240 | /** | |
| 241 | * Default dense child lookup span in code points used when materializing | |
| 242 | * compiled nodes without an explicit override. | |
| 243 | * <p> | |
| 244 | * Increasing this value increases the chance of direct array indexing for child | |
| 245 | * lookup at runtime at the cost of per-node dense table memory for compact | |
| 246 | * character spans. | |
| 247 | * </p> | |
| 248 | */ | |
| 249 | public static final int DEFAULT_MAX_EXPANDED_INDEX = 512; | |
| 250 | ||
| 251 | /** | |
| 252 | * Returns the current persisted binary stream format version. | |
| 253 | * | |
| 254 | * <p> | |
| 255 | * This method exists so other components can construct {@link TrieMetadata} | |
| 256 | * instances aligned with the currently written binary format without | |
| 257 | * duplicating constants. | |
| 258 | * </p> | |
| 259 | * | |
| 260 | * @return current trie stream format version | |
| 261 | */ | |
| 262 | public static int currentFormatVersion() { | |
| 263 |
1
1. currentFormatVersion : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::currentFormatVersion → KILLED |
return STREAM_VERSION; |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Returns whether the supplied metadata identifies a stream format with a | |
| 268 | * serialized value table. | |
| 269 | * | |
| 270 | * @param metadata parsed trie metadata | |
| 271 | * @return {@code true} when values are stored in a stream-local table | |
| 272 | * @throws NullPointerException if {@code metadata} is {@code null} | |
| 273 | */ | |
| 274 | /* default */ static boolean usesValueTableFormat(final TrieMetadata metadata) { | |
| 275 |
3
1. usesValueTableFormat : changed conditional boundary → SURVIVED 2. usesValueTableFormat : negated conditional → KILLED 3. usesValueTableFormat : replaced boolean return with true for org/egothor/stemmer/FrequencyTrie::usesValueTableFormat → KILLED |
return Objects.requireNonNull(metadata, "metadata").formatVersion() >= VALUE_TABLE_VERSION; |
| 276 | } | |
| 277 | ||
| 278 | /** | |
| 279 | * Receives trie values during visitor-style lookup. | |
| 280 | * | |
| 281 | * <p> | |
| 282 | * Implementations are caller-owned and are not retained by the trie. Returning | |
| 283 | * {@code false} stops iteration after the current callback. | |
| 284 | * </p> | |
| 285 | * | |
| 286 | * @param <V> value type | |
| 287 | */ | |
| 288 | @FunctionalInterface | |
| 289 | public interface EntrySink<V> { | |
| 290 | ||
| 291 | /** | |
| 292 | * Accepts one ordered local value. | |
| 293 | * | |
| 294 | * @param value stored value | |
| 295 | * @param count stored local occurrence count | |
| 296 | * @param rank zero-based rank in deterministic local ordering | |
| 297 | * @return {@code true} to continue iteration, {@code false} to stop | |
| 298 | */ | |
| 299 | boolean accept(V value, int count, int rank); | |
| 300 | } | |
| 301 | ||
| 302 | /** | |
| 303 | * Creates a new compiled trie instance. | |
| 304 | * | |
| 305 | * @param arrayFactory array factory | |
| 306 | * @param root compiled root node | |
| 307 | * @param metadata trie metadata describing lookup and persistence semantics | |
| 308 | * @throws NullPointerException if any argument is {@code null} | |
| 309 | */ | |
| 310 | private FrequencyTrie(final IntFunction<V[]> arrayFactory, final CompiledNode<V> root, | |
| 311 | final TrieMetadata metadata) { | |
| 312 | this.root = Objects.requireNonNull(root, "root"); | |
| 313 | this.metadata = Objects.requireNonNull(metadata, "metadata"); | |
| 314 | this.lookupTraversalDirection = metadata.traversalDirection(); | |
| 315 |
1
1. <init> : negated conditional → KILLED |
this.lowercasesLookupKeys = metadata.caseProcessingMode() == CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT; |
| 316 |
1
1. <init> : negated conditional → KILLED |
this.removeDiacritics = metadata.diacriticProcessingMode() == DiacriticProcessingMode.REMOVE; |
| 317 | this.emptyValues = arrayFactory.apply(0); | |
| 318 | this.lookupMode = LookupMode.FIRST; | |
| 319 |
1
1. <init> : negated conditional → KILLED |
this.backwardLookup = this.lookupTraversalDirection == WordTraversalDirection.BACKWARD; |
| 320 | } | |
| 321 | ||
| 322 | /** | |
| 323 | * Creates a lookup-policy view over an existing compiled trie, sharing its | |
| 324 | * immutable compiled structure and metadata. | |
| 325 | * | |
| 326 | * @param source original trie | |
| 327 | * @param mode lookup policy for the new view | |
| 328 | */ | |
| 329 | private FrequencyTrie(final FrequencyTrie<V> source, final LookupMode mode) { | |
| 330 | this.root = source.root; | |
| 331 | this.metadata = source.metadata; | |
| 332 | this.lookupTraversalDirection = source.lookupTraversalDirection; | |
| 333 | this.lowercasesLookupKeys = source.lowercasesLookupKeys; | |
| 334 | this.removeDiacritics = source.removeDiacritics; | |
| 335 | this.emptyValues = source.emptyValues; | |
| 336 | this.lookupMode = mode; | |
| 337 | this.backwardLookup = source.backwardLookup; | |
| 338 | } | |
| 339 | ||
| 340 | /** | |
| 341 | * Returns the read-time command-selection policy of this trie. | |
| 342 | * | |
| 343 | * <p> | |
| 344 | * The operation is constant-time and performs no allocation. | |
| 345 | * </p> | |
| 346 | * | |
| 347 | * @return current lookup mode | |
| 348 | */ | |
| 349 | public LookupMode lookupMode() { | |
| 350 |
1
1. lookupMode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::lookupMode → KILLED |
return this.lookupMode; |
| 351 | } | |
| 352 | ||
| 353 | /** | |
| 354 | * Returns a view of this trie that selects commands according to {@code mode}. | |
| 355 | * | |
| 356 | * <p> | |
| 357 | * The returned instance shares this trie's immutable compiled structure, so | |
| 358 | * the operation is cheap and thread-safe. The policy is a read-time concern | |
| 359 | * and is never persisted (see {@link LookupMode}). Returns {@code this} when | |
| 360 | * the mode is unchanged. | |
| 361 | * </p> | |
| 362 | * | |
| 363 | * @apiNote Serializing this view writes the shared trie structure and metadata, | |
| 364 | * but not {@code mode}. A subsequently loaded trie therefore starts in | |
| 365 | * {@link LookupMode#FIRST}. | |
| 366 | * | |
| 367 | * @param mode command-selection policy | |
| 368 | * @return a trie view applying {@code mode}; returns this instance when the | |
| 369 | * requested mode is already active | |
| 370 | * @throws NullPointerException if {@code mode} is {@code null} | |
| 371 | */ | |
| 372 | public FrequencyTrie<V> withLookupMode(final LookupMode mode) { | |
| 373 | Objects.requireNonNull(mode, "mode"); | |
| 374 |
1
1. withLookupMode : negated conditional → KILLED |
if (mode == this.lookupMode) { |
| 375 |
1
1. withLookupMode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::withLookupMode → KILLED |
return this; |
| 376 | } | |
| 377 |
1
1. withLookupMode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::withLookupMode → KILLED |
return new FrequencyTrie<>(this, mode); |
| 378 | } | |
| 379 | ||
| 380 | /** | |
| 381 | * Creates a trie from an already compiled root. | |
| 382 | * | |
| 383 | * @param arrayFactory array factory | |
| 384 | * @param root compiled root | |
| 385 | * @param metadata trie metadata | |
| 386 | * @param <V> value type | |
| 387 | * @return trie instance | |
| 388 | */ | |
| 389 | /* default */ static <V> FrequencyTrie<V> fromCompiled(final IntFunction<V[]> arrayFactory, | |
| 390 | final CompiledNode<V> root, | |
| 391 | final TrieMetadata metadata) { | |
| 392 |
1
1. fromCompiled : replaced return value with null for org/egothor/stemmer/FrequencyTrie::fromCompiled → KILLED |
return new FrequencyTrie<>(arrayFactory, root, metadata); |
| 393 | } | |
| 394 | ||
| 395 | /** | |
| 396 | * Returns the preferred value selected for the supplied key under this trie's | |
| 397 | * {@linkplain #lookupMode() lookup mode}. | |
| 398 | * | |
| 399 | * <p> | |
| 400 | * If multiple values have the same local frequency, the returned value is | |
| 401 | * selected deterministically by shorter {@code toString()} value first, then by | |
| 402 | * lexicographically lower {@code toString()}, and finally by stable first-seen | |
| 403 | * order. | |
| 404 | * | |
| 405 | * <p> | |
| 406 | * The supplied key is normalized according to persisted | |
| 407 | * {@link TrieMetadata#caseProcessingMode()} before traversal. | |
| 408 | * In {@link LookupMode#ALL}, this scalar operation uses the same | |
| 409 | * most-specific selection as {@link LookupMode#LAST}. | |
| 410 | * </p> | |
| 411 | * | |
| 412 | * @param key key to resolve | |
| 413 | * @return most frequent value, or {@code null} if the key does not exist or no | |
| 414 | * value is stored at the addressed node | |
| 415 | * @throws NullPointerException if {@code key} is {@code null} | |
| 416 | */ | |
| 417 | public V get(final String key) { | |
| 418 | Objects.requireNonNull(key, ARG_KEY); | |
| 419 | final String normalized = normalizeLookupKey(key); | |
| 420 |
1
1. get : negated conditional → KILLED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 421 | ? findNode(normalized) | |
| 422 | : TrieLookup.findLast(this.root, this.backwardLookup, normalized); | |
| 423 |
1
1. get : negated conditional → KILLED |
if (node == null) { |
| 424 | return null; | |
| 425 | } | |
| 426 | final V[] orderedValues = node.orderedValues(); | |
| 427 |
1
1. get : negated conditional → KILLED |
if (orderedValues.length == 0) { |
| 428 | return null; | |
| 429 | } | |
| 430 |
1
1. get : replaced return value with null for org/egothor/stemmer/FrequencyTrie::get → KILLED |
return orderedValues[0]; |
| 431 | } | |
| 432 | ||
| 433 | /** | |
| 434 | * Returns the preferred value for an already-normalized key. | |
| 435 | * | |
| 436 | * <p> | |
| 437 | * This method bypasses {@link TrieMetadata#caseProcessingMode()} and | |
| 438 | * {@link TrieMetadata#diacriticProcessingMode()}. Callers must supply input | |
| 439 | * normalized exactly as required by this trie's metadata. It is intended for | |
| 440 | * hot paths where normalization is guaranteed by an upstream tokenizer or | |
| 441 | * benchmark corpus and repeated lookup-time normalization would be redundant. | |
| 442 | * </p> | |
| 443 | * | |
| 444 | * @param key already-normalized key to resolve | |
| 445 | * @return most frequent value, or {@code null} if the key does not exist or no | |
| 446 | * value is stored at the addressed node | |
| 447 | * @throws NullPointerException if {@code key} is {@code null} | |
| 448 | */ | |
| 449 | public V getNormalized(final CharSequence key) { | |
| 450 | Objects.requireNonNull(key, ARG_KEY); | |
| 451 |
1
1. getNormalized : negated conditional → SURVIVED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 452 | ? findNode(key) | |
| 453 | : TrieLookup.findLast(this.root, this.backwardLookup, key); | |
| 454 |
1
1. getNormalized : negated conditional → KILLED |
if (node == null) { |
| 455 | return null; | |
| 456 | } | |
| 457 | final V[] orderedValues = node.orderedValues(); | |
| 458 |
1
1. getNormalized : negated conditional → KILLED |
if (orderedValues.length == 0) { |
| 459 | return null; | |
| 460 | } | |
| 461 |
1
1. getNormalized : replaced return value with null for org/egothor/stemmer/FrequencyTrie::getNormalized → KILLED |
return orderedValues[0]; |
| 462 | } | |
| 463 | ||
| 464 | /** | |
| 465 | * Returns the preferred value for an already-normalized {@link String} key. | |
| 466 | * | |
| 467 | * <p> | |
| 468 | * This overload keeps high-volume string lookup on a monomorphic path and | |
| 469 | * avoids the {@link CharSequence} dispatch used by the general overload. | |
| 470 | * Callers must supply input normalized exactly as required by this trie's | |
| 471 | * metadata. | |
| 472 | * </p> | |
| 473 | * | |
| 474 | * @param key already-normalized key to resolve | |
| 475 | * @return most frequent value, or {@code null} if the key does not exist or no | |
| 476 | * value is stored at the addressed node | |
| 477 | * @throws NullPointerException if {@code key} is {@code null} | |
| 478 | */ | |
| 479 | public V getNormalizedString(final String key) { | |
| 480 | Objects.requireNonNull(key, ARG_KEY); | |
| 481 |
1
1. getNormalizedString : negated conditional → SURVIVED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 482 | ? findNode(key) | |
| 483 | : TrieLookup.findLast(this.root, this.backwardLookup, key); | |
| 484 |
1
1. getNormalizedString : negated conditional → KILLED |
if (node == null) { |
| 485 | return null; | |
| 486 | } | |
| 487 | final V[] orderedValues = node.orderedValues(); | |
| 488 |
1
1. getNormalizedString : negated conditional → KILLED |
if (orderedValues.length == 0) { |
| 489 | return null; | |
| 490 | } | |
| 491 |
1
1. getNormalizedString : replaced return value with null for org/egothor/stemmer/FrequencyTrie::getNormalizedString → KILLED |
return orderedValues[0]; |
| 492 | } | |
| 493 | ||
| 494 | /** | |
| 495 | * Returns the values selected for the supplied key under this trie's | |
| 496 | * {@linkplain #lookupMode() lookup mode}. | |
| 497 | * | |
| 498 | * <p> | |
| 499 | * If multiple values have the same local frequency, the ordering is | |
| 500 | * deterministic by shorter {@code toString()} value first, then by | |
| 501 | * lexicographically lower {@code toString()}, and finally by stable first-seen | |
| 502 | * order. | |
| 503 | * | |
| 504 | * <p> | |
| 505 | * The returned array is a defensive copy. | |
| 506 | * Under {@link LookupMode#ALL}, values from all applicable nodes are ordered | |
| 507 | * from the most-specific node to the least-specific accepting ancestor, | |
| 508 | * retain each node's local frequency order, and are de-duplicated by | |
| 509 | * {@link Object#equals(Object)}. Other modes return one selected node's values. | |
| 510 | * </p> | |
| 511 | * | |
| 512 | * <p> | |
| 513 | * The supplied key is normalized according to persisted | |
| 514 | * {@link TrieMetadata#caseProcessingMode()} before traversal. | |
| 515 | * </p> | |
| 516 | * | |
| 517 | * @param key key to resolve | |
| 518 | * @return all values stored at the addressed node, ordered by descending | |
| 519 | * frequency; returns an empty array if the key does not exist or no | |
| 520 | * value is stored at the addressed node | |
| 521 | * @throws NullPointerException if {@code key} is {@code null} | |
| 522 | */ | |
| 523 | @SuppressWarnings("PMD.MethodReturnsInternalArray") | |
| 524 | public V[] getAll(final String key) { | |
| 525 | Objects.requireNonNull(key, ARG_KEY); | |
| 526 | final String normalized = normalizeLookupKey(key); | |
| 527 |
1
1. getAll : negated conditional → KILLED |
if (this.lookupMode == LookupMode.ALL) { |
| 528 |
1
1. getAll : replaced return value with null for org/egothor/stemmer/FrequencyTrie::getAll → KILLED |
return TrieLookup.collectAllValues( |
| 529 | TrieLookup.collectPath(this.root, this.backwardLookup, normalized), this.emptyValues); | |
| 530 | } | |
| 531 |
1
1. getAll : negated conditional → KILLED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 532 | ? findNode(normalized) | |
| 533 | : TrieLookup.findLast(this.root, this.backwardLookup, normalized); | |
| 534 |
1
1. getAll : negated conditional → KILLED |
if (node == null) { |
| 535 |
1
1. getAll : replaced return value with null for org/egothor/stemmer/FrequencyTrie::getAll → KILLED |
return this.emptyValues; |
| 536 | } | |
| 537 | final V[] orderedValues = node.orderedValues(); | |
| 538 |
1
1. getAll : negated conditional → KILLED |
if (orderedValues.length == 0) { |
| 539 |
1
1. getAll : replaced return value with null for org/egothor/stemmer/FrequencyTrie::getAll → SURVIVED |
return this.emptyValues; |
| 540 | } | |
| 541 |
1
1. getAll : replaced return value with null for org/egothor/stemmer/FrequencyTrie::getAll → KILLED |
return Arrays.copyOf(orderedValues, orderedValues.length); |
| 542 | } | |
| 543 | ||
| 544 | /** | |
| 545 | * Returns the selected values and their occurrence counts, ordered by the same | |
| 546 | * rules as {@link #getAll(String)}. | |
| 547 | * | |
| 548 | * <p> | |
| 549 | * The returned list is aligned with the arrays returned by | |
| 550 | * {@link #getAll(String)} and the internal compiled count representation. | |
| 551 | * | |
| 552 | * <p> | |
| 553 | * The returned list is immutable. | |
| 554 | * | |
| 555 | * <p> | |
| 556 | * In reduction modes that merge semantically equivalent subtrees, the returned | |
| 557 | * counts may be aggregated across multiple original build-time nodes that were | |
| 558 | * reduced into the same canonical compiled node. | |
| 559 | * Under {@link LookupMode#ALL}, a value occurring at multiple applicable nodes | |
| 560 | * appears once with the count from its most-specific occurrence. | |
| 561 | * </p> | |
| 562 | * | |
| 563 | * @param key key to resolve | |
| 564 | * @return immutable ordered list of value-count entries; returns an empty list | |
| 565 | * if the key does not exist or no value is stored at the addressed node | |
| 566 | * @throws NullPointerException if {@code key} is {@code null} | |
| 567 | */ | |
| 568 | public List<ValueCount<V>> getEntries(final String key) { | |
| 569 | Objects.requireNonNull(key, ARG_KEY); | |
| 570 | final String normalized = normalizeLookupKey(key); | |
| 571 |
1
1. getEntries : negated conditional → KILLED |
if (this.lookupMode == LookupMode.ALL) { |
| 572 |
1
1. getEntries : replaced return value with Collections.emptyList for org/egothor/stemmer/FrequencyTrie::getEntries → KILLED |
return TrieLookup.collectAllEntries( |
| 573 | TrieLookup.collectPath(this.root, this.backwardLookup, normalized)); | |
| 574 | } | |
| 575 |
1
1. getEntries : negated conditional → SURVIVED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 576 | ? findNode(normalized) | |
| 577 | : TrieLookup.findLast(this.root, this.backwardLookup, normalized); | |
| 578 |
1
1. getEntries : negated conditional → KILLED |
if (node == null) { |
| 579 | return List.of(); | |
| 580 | } | |
| 581 | ||
| 582 | final V[] orderedValues = node.orderedValues(); | |
| 583 | final int valueCount = orderedValues.length; | |
| 584 |
1
1. getEntries : negated conditional → KILLED |
if (valueCount == NO_VALUE_COUNT) { |
| 585 | return List.of(); | |
| 586 | } | |
| 587 | ||
| 588 |
1
1. getEntries : negated conditional → KILLED |
if (valueCount == SINGLE_VALUE_COUNT) { |
| 589 |
1
1. getEntries : replaced return value with Collections.emptyList for org/egothor/stemmer/FrequencyTrie::getEntries → KILLED |
return List.of(new ValueCount<>(orderedValues[0], node.orderedCounts()[0])); |
| 590 | } | |
| 591 | ||
| 592 | final int[] orderedCounts = node.orderedCounts(); | |
| 593 | final List<ValueCount<V>> entries = new ArrayList<>(valueCount); | |
| 594 |
2
1. getEntries : changed conditional boundary → KILLED 2. getEntries : negated conditional → KILLED |
for (int index = 0; index < valueCount; index++) { |
| 595 | entries.add(new ValueCount<>(orderedValues[index], orderedCounts[index])); | |
| 596 | } | |
| 597 |
1
1. getEntries : replaced return value with Collections.emptyList for org/egothor/stemmer/FrequencyTrie::getEntries → KILLED |
return Collections.unmodifiableList(entries); |
| 598 | } | |
| 599 | ||
| 600 | /** | |
| 601 | * Visits all values stored at the node addressed by an already-normalized | |
| 602 | * {@code char[]} key slice. | |
| 603 | * | |
| 604 | * <p> | |
| 605 | * This method bypasses {@link TrieMetadata#caseProcessingMode()} and | |
| 606 | * {@link TrieMetadata#diacriticProcessingMode()}. The caller must provide input | |
| 607 | * normalized exactly as required by this trie's metadata. The trie is immutable | |
| 608 | * and thread-safe for concurrent reads; the supplied sink is caller-owned and | |
| 609 | * is not retained. | |
| 610 | * </p> | |
| 611 | * | |
| 612 | * @param key normalized key storage | |
| 613 | * @param offset first character offset | |
| 614 | * @param length number of characters to read | |
| 615 | * @param sink value sink | |
| 616 | * @param maxResults maximum number of results to visit | |
| 617 | * @return number of visited values | |
| 618 | * @throws NullPointerException if {@code key} or {@code sink} is | |
| 619 | * {@code null} | |
| 620 | * @throws IndexOutOfBoundsException if the key slice is invalid | |
| 621 | * @throws IllegalArgumentException if {@code maxResults} is negative | |
| 622 | */ | |
| 623 | public int getAllNormalized(final char[] key, final int offset, final int length, final EntrySink<? super V> sink, | |
| 624 | final int maxResults) { | |
| 625 | Objects.requireNonNull(key, ARG_KEY); | |
| 626 | Objects.requireNonNull(sink, "sink"); | |
| 627 | Objects.checkFromIndexSize(offset, length, key.length); | |
| 628 |
1
1. getAllNormalized : removed call to org/egothor/stemmer/FrequencyTrie::validateMaxResults → SURVIVED |
validateMaxResults(maxResults); |
| 629 |
1
1. getAllNormalized : negated conditional → KILLED |
if (maxResults == 0) { |
| 630 | return 0; | |
| 631 | } | |
| 632 |
1
1. getAllNormalized : negated conditional → SURVIVED |
if (this.lookupMode == LookupMode.ALL) { |
| 633 |
1
1. getAllNormalized : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::getAllNormalized → NO_COVERAGE |
return TrieLookup.visitNodes( |
| 634 | TrieLookup.collectPath(this.root, this.backwardLookup, CharBuffer.wrap(key, offset, length)), | |
| 635 | sink, maxResults); | |
| 636 | } | |
| 637 |
1
1. getAllNormalized : negated conditional → SURVIVED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 638 | ? findNode(key, offset, length) | |
| 639 | : TrieLookup.findLast(this.root, this.backwardLookup, key, offset, length); | |
| 640 |
1
1. getAllNormalized : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::getAllNormalized → KILLED |
return visitNode(node, sink, maxResults); |
| 641 | } | |
| 642 | ||
| 643 | /** | |
| 644 | * Visits all values stored at the node addressed by an already-normalized | |
| 645 | * character sequence. | |
| 646 | * | |
| 647 | * @param key normalized key | |
| 648 | * @param sink value sink | |
| 649 | * @param maxResults maximum number of results to visit | |
| 650 | * @return number of visited values | |
| 651 | * @throws NullPointerException if {@code key} or {@code sink} is | |
| 652 | * {@code null} | |
| 653 | * @throws IllegalArgumentException if {@code maxResults} is negative | |
| 654 | * @see #getAllNormalized(char[], int, int, EntrySink, int) | |
| 655 | */ | |
| 656 | public int getAllNormalized(final CharSequence key, final EntrySink<? super V> sink, final int maxResults) { | |
| 657 | Objects.requireNonNull(key, ARG_KEY); | |
| 658 | Objects.requireNonNull(sink, "sink"); | |
| 659 |
1
1. getAllNormalized : removed call to org/egothor/stemmer/FrequencyTrie::validateMaxResults → KILLED |
validateMaxResults(maxResults); |
| 660 |
1
1. getAllNormalized : negated conditional → KILLED |
if (maxResults == 0) { |
| 661 | return 0; | |
| 662 | } | |
| 663 |
1
1. getAllNormalized : negated conditional → SURVIVED |
if (this.lookupMode == LookupMode.ALL) { |
| 664 |
1
1. getAllNormalized : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::getAllNormalized → NO_COVERAGE |
return TrieLookup.visitNodes( |
| 665 | TrieLookup.collectPath(this.root, this.backwardLookup, key), sink, maxResults); | |
| 666 | } | |
| 667 |
1
1. getAllNormalized : negated conditional → SURVIVED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 668 | ? findNode(key) | |
| 669 | : TrieLookup.findLast(this.root, this.backwardLookup, key); | |
| 670 |
1
1. getAllNormalized : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::getAllNormalized → KILLED |
return visitNode(node, sink, maxResults); |
| 671 | } | |
| 672 | ||
| 673 | /** | |
| 674 | * Visits the first value stored at the node addressed by an already-normalized | |
| 675 | * {@code char[]} key slice. | |
| 676 | * | |
| 677 | * @param key normalized key storage | |
| 678 | * @param offset first character offset | |
| 679 | * @param length number of characters to read | |
| 680 | * @param sink value sink | |
| 681 | * @return {@code true} when a value was visited, otherwise {@code false} | |
| 682 | * @see #getAllNormalized(char[], int, int, EntrySink, int) | |
| 683 | */ | |
| 684 | public boolean getFirstNormalized(final char[] key, final int offset, final int length, | |
| 685 | final EntrySink<? super V> sink) { | |
| 686 |
2
1. getFirstNormalized : replaced boolean return with true for org/egothor/stemmer/FrequencyTrie::getFirstNormalized → NO_COVERAGE 2. getFirstNormalized : negated conditional → NO_COVERAGE |
return getAllNormalized(key, offset, length, sink, 1) == 1; |
| 687 | } | |
| 688 | ||
| 689 | /** | |
| 690 | * Visits the first value stored at the node addressed by an already-normalized | |
| 691 | * character sequence. | |
| 692 | * | |
| 693 | * @param key normalized key | |
| 694 | * @param sink value sink | |
| 695 | * @return {@code true} when a value was visited, otherwise {@code false} | |
| 696 | * @see #getAllNormalized(CharSequence, EntrySink, int) | |
| 697 | */ | |
| 698 | public boolean getFirstNormalized(final CharSequence key, final EntrySink<? super V> sink) { | |
| 699 |
2
1. getFirstNormalized : negated conditional → KILLED 2. getFirstNormalized : replaced boolean return with true for org/egothor/stemmer/FrequencyTrie::getFirstNormalized → KILLED |
return getAllNormalized(key, sink, 1) == 1; |
| 700 | } | |
| 701 | ||
| 702 | /** | |
| 703 | * Visits all values stored at the node addressed by the supplied key, applying | |
| 704 | * metadata-driven lookup normalization when required. | |
| 705 | * | |
| 706 | * <p> | |
| 707 | * This method preserves the same lookup normalization semantics as | |
| 708 | * {@link #getAll(String)}. It may allocate when metadata requires lowercase or | |
| 709 | * diacritic normalization. | |
| 710 | * </p> | |
| 711 | * | |
| 712 | * @param key key to resolve | |
| 713 | * @param sink value sink | |
| 714 | * @param maxResults maximum number of results to visit | |
| 715 | * @return number of visited values | |
| 716 | */ | |
| 717 | public int getAll(final CharSequence key, final EntrySink<? super V> sink, final int maxResults) { | |
| 718 | Objects.requireNonNull(key, ARG_KEY); | |
| 719 | Objects.requireNonNull(sink, "sink"); | |
| 720 |
1
1. getAll : removed call to org/egothor/stemmer/FrequencyTrie::validateMaxResults → KILLED |
validateMaxResults(maxResults); |
| 721 |
1
1. getAll : negated conditional → KILLED |
if (maxResults == 0) { |
| 722 | return 0; | |
| 723 | } | |
| 724 | final CharSequence normalized = normalizeLookupKey(key); | |
| 725 |
1
1. getAll : negated conditional → SURVIVED |
if (this.lookupMode == LookupMode.ALL) { |
| 726 |
1
1. getAll : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::getAll → NO_COVERAGE |
return TrieLookup.visitNodes( |
| 727 | TrieLookup.collectPath(this.root, this.backwardLookup, normalized), sink, maxResults); | |
| 728 | } | |
| 729 |
1
1. getAll : negated conditional → SURVIVED |
final CompiledNode<V> node = this.lookupMode == LookupMode.FIRST |
| 730 | ? findNode(normalized) | |
| 731 | : TrieLookup.findLast(this.root, this.backwardLookup, normalized); | |
| 732 |
1
1. getAll : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::getAll → KILLED |
return visitNode(node, sink, maxResults); |
| 733 | } | |
| 734 | ||
| 735 | /** | |
| 736 | * Visits the first value stored at the node addressed by the supplied key, | |
| 737 | * applying metadata-driven lookup normalization when required. | |
| 738 | * | |
| 739 | * @param key key to resolve | |
| 740 | * @param sink value sink | |
| 741 | * @return {@code true} when a value was visited, otherwise {@code false} | |
| 742 | * @see #getAll(CharSequence, EntrySink, int) | |
| 743 | */ | |
| 744 | public boolean getFirst(final CharSequence key, final EntrySink<? super V> sink) { | |
| 745 |
2
1. getFirst : replaced boolean return with true for org/egothor/stemmer/FrequencyTrie::getFirst → SURVIVED 2. getFirst : negated conditional → KILLED |
return getAll(key, sink, 1) == 1; |
| 746 | } | |
| 747 | ||
| 748 | /** | |
| 749 | * Returns the logical key traversal direction used by this trie. | |
| 750 | * | |
| 751 | * <p> | |
| 752 | * The same direction must be used when reconstructing mutable builders or when | |
| 753 | * applying patch commands that were generated against keys stored in this trie. | |
| 754 | * </p> | |
| 755 | * | |
| 756 | * @return logical key traversal direction | |
| 757 | */ | |
| 758 | public WordTraversalDirection traversalDirection() { | |
| 759 |
1
1. traversalDirection : replaced return value with null for org/egothor/stemmer/FrequencyTrie::traversalDirection → KILLED |
return this.metadata.traversalDirection(); |
| 760 | } | |
| 761 | ||
| 762 | /** | |
| 763 | * Returns immutable persisted metadata associated with this trie. | |
| 764 | * | |
| 765 | * @return trie metadata | |
| 766 | */ | |
| 767 | public TrieMetadata metadata() { | |
| 768 |
1
1. metadata : replaced return value with null for org/egothor/stemmer/FrequencyTrie::metadata → KILLED |
return this.metadata; |
| 769 | } | |
| 770 | ||
| 771 | /** | |
| 772 | * Returns the deterministic SHA-256 fingerprint of this trie. | |
| 773 | * | |
| 774 | * <p> | |
| 775 | * The fingerprint is a canonical model identity, not a Java object identity. It | |
| 776 | * includes a fingerprint-domain marker, the fingerprint input format version, | |
| 777 | * persisted metadata, and the complete compiled-node structure reachable from | |
| 778 | * the root, including edges, child references, local values, and local counts. | |
| 779 | * </p> | |
| 780 | * | |
| 781 | * <p> | |
| 782 | * The returned value is stable across JVM runs for equivalent trie content and | |
| 783 | * metadata. It does not include object identity, memory layout, runtime cache | |
| 784 | * state, absolute file paths, timestamps, or other process-local state. | |
| 785 | * </p> | |
| 786 | * | |
| 787 | * <p> | |
| 788 | * The fingerprint is calculated on first request and reused by later | |
| 789 | * fingerprint accessors. | |
| 790 | * </p> | |
| 791 | * | |
| 792 | * @return 64-character lowercase hexadecimal SHA-256 fingerprint | |
| 793 | */ | |
| 794 | public String getFingerprint() { | |
| 795 |
1
1. getFingerprint : replaced return value with "" for org/egothor/stemmer/FrequencyTrie::getFingerprint → KILLED |
return toLowerHex(fingerprintBytes()); |
| 796 | } | |
| 797 | ||
| 798 | /** | |
| 799 | * Returns a defensive copy of the raw SHA-256 fingerprint bytes. | |
| 800 | * | |
| 801 | * <p> | |
| 802 | * The returned array has length {@code 32}. Mutating it does not affect this | |
| 803 | * trie. | |
| 804 | * </p> | |
| 805 | * | |
| 806 | * <p> | |
| 807 | * The fingerprint is calculated on first request and reused by later | |
| 808 | * fingerprint accessors. | |
| 809 | * </p> | |
| 810 | * | |
| 811 | * @return defensive copy of the 32-byte SHA-256 fingerprint | |
| 812 | */ | |
| 813 | public byte[] copyFingerprintBytes() { | |
| 814 | final byte[] localFingerprintBytes = fingerprintBytes(); | |
| 815 |
1
1. copyFingerprintBytes : replaced return value with null for org/egothor/stemmer/FrequencyTrie::copyFingerprintBytes → KILLED |
return Arrays.copyOf(localFingerprintBytes, localFingerprintBytes.length); |
| 816 | } | |
| 817 | ||
| 818 | /** | |
| 819 | * Returns the cached raw SHA-256 fingerprint bytes, computing them on the first | |
| 820 | * request. | |
| 821 | * | |
| 822 | * <p> | |
| 823 | * Access to the cache is guarded by {@link #fingerprintLock}. Because every read | |
| 824 | * and write of the cache occurs while holding the same lock, successful | |
| 825 | * initialization is safely published to all subsequent callers. If calculation | |
| 826 | * fails, no value is cached and a later call may retry. | |
| 827 | * </p> | |
| 828 | * | |
| 829 | * @return internal cached raw SHA-256 fingerprint bytes | |
| 830 | */ | |
| 831 | private byte[] fingerprintBytes() { | |
| 832 |
1
1. fingerprintBytes : removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED |
this.fingerprintLock.lock(); |
| 833 | try { | |
| 834 | byte[] localFingerprintBytes = this.fingerprintBytes; | |
| 835 |
1
1. fingerprintBytes : negated conditional → KILLED |
if (localFingerprintBytes == null) { |
| 836 | localFingerprintBytes = computeFingerprintBytes(this.root, this.metadata); | |
| 837 | this.fingerprintBytes = localFingerprintBytes; | |
| 838 | } | |
| 839 |
1
1. fingerprintBytes : replaced return value with null for org/egothor/stemmer/FrequencyTrie::fingerprintBytes → KILLED |
return localFingerprintBytes; |
| 840 | } finally { | |
| 841 |
1
1. fingerprintBytes : removed call to java/util/concurrent/locks/ReentrantLock::unlock → TIMED_OUT |
this.fingerprintLock.unlock(); |
| 842 | } | |
| 843 | } | |
| 844 | ||
| 845 | private static <V> byte[] computeFingerprintBytes(final CompiledNode<V> root, final TrieMetadata metadata) { | |
| 846 | final MessageDigest messageDigest = newSha256Digest(); | |
| 847 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::updateUtf8 → SURVIVED |
updateUtf8(messageDigest, FINGERPRINT_DOMAIN); |
| 848 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, FINGERPRINT_FORMAT_VERSION); |
| 849 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::updateUtf8 → SURVIVED |
updateUtf8(messageDigest, metadata.toTextBlock()); |
| 850 | ||
| 851 | final Map<CompiledNode<V>, Integer> nodeIds = new IdentityHashMap<>(); | |
| 852 | final List<CompiledNode<V>> orderedNodes = new ArrayList<>(); | |
| 853 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::assignNodeIds → KILLED |
assignNodeIds(root, nodeIds, orderedNodes); |
| 854 | ||
| 855 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, nodeIds.get(root)); |
| 856 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, orderedNodes.size()); |
| 857 | for (CompiledNode<V> node : orderedNodes) { | |
| 858 |
1
1. computeFingerprintBytes : removed call to org/egothor/stemmer/FrequencyTrie::updateNodeFingerprint → KILLED |
updateNodeFingerprint(messageDigest, node, nodeIds); |
| 859 | } | |
| 860 |
1
1. computeFingerprintBytes : replaced return value with null for org/egothor/stemmer/FrequencyTrie::computeFingerprintBytes → KILLED |
return messageDigest.digest(); |
| 861 | } | |
| 862 | ||
| 863 | /** | |
| 864 | * Returns the root node mainly for diagnostics and tests within the package. | |
| 865 | * | |
| 866 | * @return compiled root node | |
| 867 | */ | |
| 868 | /* default */ CompiledNode<V> root() { | |
| 869 |
1
1. root : replaced return value with null for org/egothor/stemmer/FrequencyTrie::root → KILLED |
return this.root; |
| 870 | } | |
| 871 | ||
| 872 | /** | |
| 873 | * Writes this compiled trie to the supplied output stream. | |
| 874 | * | |
| 875 | * <p> | |
| 876 | * The binary format is versioned and preserves canonical shared compiled nodes, | |
| 877 | * therefore the serialized representation remains compact even for tries | |
| 878 | * reduced by subtree merging. | |
| 879 | * | |
| 880 | * <p> | |
| 881 | * The supplied codec is responsible for persisting individual values of type | |
| 882 | * {@code V}. | |
| 883 | * | |
| 884 | * @param outputStream target output stream | |
| 885 | * @param valueCodec codec used to write values | |
| 886 | * @throws NullPointerException if any argument is {@code null} | |
| 887 | * @throws IOException if writing fails | |
| 888 | */ | |
| 889 | public void writeTo(final OutputStream outputStream, final ValueStreamCodec<V> valueCodec) throws IOException { | |
| 890 | Objects.requireNonNull(outputStream, "outputStream"); | |
| 891 | Objects.requireNonNull(valueCodec, "valueCodec"); | |
| 892 | ||
| 893 | final DataOutputStream dataOutput; // NOPMD | |
| 894 |
1
1. writeTo : negated conditional → KILLED |
if (outputStream instanceof DataOutputStream) { |
| 895 | dataOutput = (DataOutputStream) outputStream; | |
| 896 | } else { | |
| 897 | dataOutput = new DataOutputStream(outputStream); | |
| 898 | } | |
| 899 | ||
| 900 | final Map<CompiledNode<V>, Integer> nodeIds = new IdentityHashMap<>(); | |
| 901 | final List<CompiledNode<V>> orderedNodes = new ArrayList<>(); | |
| 902 |
1
1. writeTo : removed call to org/egothor/stemmer/FrequencyTrie::assignNodeIds → KILLED |
assignNodeIds(this.root, nodeIds, orderedNodes); |
| 903 | final Map<V, Integer> valueIds = new LinkedHashMap<>(); | |
| 904 | final List<V> distinctValues = new ArrayList<>(); | |
| 905 |
1
1. writeTo : removed call to org/egothor/stemmer/FrequencyTrie::collectDistinctValues → KILLED |
collectDistinctValues(orderedNodes, valueIds, distinctValues); |
| 906 | ||
| 907 | if (LOGGER.isLoggable(Level.FINE)) { | |
| 908 | LOGGER.log(Level.FINE, "Writing compiled trie with {0} canonical nodes.", orderedNodes.size()); | |
| 909 | } | |
| 910 | ||
| 911 |
1
1. writeTo : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(STREAM_MAGIC); |
| 912 |
1
1. writeTo : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(STREAM_VERSION); |
| 913 |
1
1. writeTo : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(orderedNodes.size()); |
| 914 |
1
1. writeTo : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(nodeIds.get(this.root)); |
| 915 |
1
1. writeTo : removed call to org/egothor/stemmer/FrequencyTrie::writeMetadata → KILLED |
writeMetadata(dataOutput, metadataForCurrentStream(this.metadata)); |
| 916 |
1
1. writeTo : removed call to org/egothor/stemmer/FrequencyTrie::writeValueTable → KILLED |
writeValueTable(dataOutput, valueCodec, distinctValues); |
| 917 | ||
| 918 |
2
1. writeTo : negated conditional → KILLED 2. writeTo : changed conditional boundary → KILLED |
for (int nodeId = 0; nodeId < orderedNodes.size(); nodeId++) { |
| 919 |
1
1. writeTo : removed call to org/egothor/stemmer/FrequencyTrie::writeNode → KILLED |
writeNode(dataOutput, orderedNodes.get(nodeId), nodeId, nodeIds, valueIds); |
| 920 | } | |
| 921 | ||
| 922 |
1
1. writeTo : removed call to java/io/DataOutputStream::flush → SURVIVED |
dataOutput.flush(); |
| 923 | } | |
| 924 | ||
| 925 | /** | |
| 926 | * Reads a compiled trie from the supplied input stream. | |
| 927 | * | |
| 928 | * <p> | |
| 929 | * The caller must provide the same value codec semantics that were used during | |
| 930 | * persistence as well as the array factory required for typed result arrays. | |
| 931 | * | |
| 932 | * @param inputStream source input stream | |
| 933 | * @param arrayFactory factory used to create typed arrays | |
| 934 | * @param valueCodec codec used to read values | |
| 935 | * @param <V> value type | |
| 936 | * @return deserialized compiled trie | |
| 937 | * @throws NullPointerException if any argument is {@code null} | |
| 938 | * @throws IOException if reading fails or the binary format is invalid | |
| 939 | */ | |
| 940 | public static <V> FrequencyTrie<V> readFrom(final InputStream inputStream, final IntFunction<V[]> arrayFactory, | |
| 941 | final ValueStreamCodec<V> valueCodec) throws IOException { | |
| 942 |
1
1. readFrom : replaced return value with null for org/egothor/stemmer/FrequencyTrie::readFrom → KILLED |
return readFrom(inputStream, arrayFactory, valueCodec, -1); |
| 943 | } | |
| 944 | ||
| 945 | /** | |
| 946 | * Reads a compiled trie from the supplied input stream, optionally overriding | |
| 947 | * dense child-index span configuration. | |
| 948 | * <p> | |
| 949 | * This setting is applied only while materializing the in-memory compiled | |
| 950 | * representation during load. It is not serialized in {@link TrieMetadata}, so | |
| 951 | * each load can independently choose its own runtime lookup trade-off. | |
| 952 | * </p> | |
| 953 | * | |
| 954 | * @param inputStream source input stream | |
| 955 | * @param arrayFactory array factory used to create typed arrays | |
| 956 | * @param valueCodec codec used to read values | |
| 957 | * @param maxExpandedIndex dense lookup span override; zero disables dense | |
| 958 | * lookup, negative values use | |
| 959 | * {@link #DEFAULT_MAX_EXPANDED_INDEX} | |
| 960 | * @param <V> value type | |
| 961 | * @return deserialized compiled trie | |
| 962 | * @throws NullPointerException if any argument is {@code null} | |
| 963 | * @throws IOException if reading fails or the binary format is invalid | |
| 964 | */ | |
| 965 | public static <V> FrequencyTrie<V> readFrom(final InputStream inputStream, final IntFunction<V[]> arrayFactory, | |
| 966 | final ValueStreamCodec<V> valueCodec, final int maxExpandedIndex) throws IOException { | |
| 967 | Objects.requireNonNull(valueCodec, "valueCodec"); | |
| 968 |
1
1. readFrom : replaced return value with null for org/egothor/stemmer/FrequencyTrie::readFrom → KILLED |
return readFromWithMetadata(inputStream, arrayFactory, |
| 969 |
1
1. lambda$readFrom$0 : replaced return value with null for org/egothor/stemmer/FrequencyTrie::lambda$readFrom$0 → KILLED |
(dataInput, metadata) -> valueCodec.read(dataInput), maxExpandedIndex); |
| 970 | } | |
| 971 | ||
| 972 | /** | |
| 973 | * Reads a compiled trie while allowing value decoding to use already parsed | |
| 974 | * trie metadata. | |
| 975 | * | |
| 976 | * <p> | |
| 977 | * This package-private path materializes the requested final value type during | |
| 978 | * the normal graph read. It does not expose reader state or construct an | |
| 979 | * intermediate trie with a different value type. | |
| 980 | * </p> | |
| 981 | * | |
| 982 | * @param inputStream source input stream | |
| 983 | * @param arrayFactory factory used to create typed value arrays | |
| 984 | * @param valueReader metadata-aware value reader | |
| 985 | * @param maxExpandedIndex dense lookup span override; zero disables dense | |
| 986 | * lookup, negative values use | |
| 987 | * {@link #DEFAULT_MAX_EXPANDED_INDEX} | |
| 988 | * @param <V> final value type | |
| 989 | * @return deserialized compiled trie containing values returned by | |
| 990 | * {@code valueReader} | |
| 991 | * @throws NullPointerException if any argument is {@code null} | |
| 992 | * @throws IOException if reading fails or the binary format is invalid | |
| 993 | */ | |
| 994 | /* default */ static <V> FrequencyTrie<V> readFromWithMetadata(final InputStream inputStream, | |
| 995 | final IntFunction<V[]> arrayFactory, final MetadataValueStreamReader<V> valueReader, | |
| 996 | final int maxExpandedIndex) throws IOException { | |
| 997 |
1
1. readFromWithMetadata : replaced return value with null for org/egothor/stemmer/FrequencyTrie::readFromWithMetadata → KILLED |
return CompiledTrieReader.read(inputStream, arrayFactory, valueReader, maxExpandedIndex); |
| 998 | } | |
| 999 | ||
| 1000 | /** | |
| 1001 | * Writes persisted trie metadata. | |
| 1002 | * | |
| 1003 | * @param dataOutput output stream | |
| 1004 | * @param metadata metadata to serialize | |
| 1005 | * @throws IOException if writing fails | |
| 1006 | */ | |
| 1007 | private static void writeMetadata(final DataOutputStream dataOutput, final TrieMetadata metadata) | |
| 1008 | throws IOException { | |
| 1009 |
1
1. writeMetadata : removed call to java/io/DataOutputStream::writeUTF → KILLED |
dataOutput.writeUTF(metadata.toTextBlock()); |
| 1010 | } | |
| 1011 | ||
| 1012 | /** | |
| 1013 | * Creates metadata aligned with the stream version emitted by the current | |
| 1014 | * writer. | |
| 1015 | * | |
| 1016 | * <p> | |
| 1017 | * The returned metadata preserves every semantic setting from the trie while | |
| 1018 | * reporting the current binary format version. The immutable metadata stored by | |
| 1019 | * the trie is not modified. | |
| 1020 | * </p> | |
| 1021 | * | |
| 1022 | * @param metadata source trie metadata | |
| 1023 | * @return metadata aligned with {@link #STREAM_VERSION} | |
| 1024 | */ | |
| 1025 | private static TrieMetadata metadataForCurrentStream(final TrieMetadata metadata) { | |
| 1026 |
1
1. metadataForCurrentStream : replaced return value with null for org/egothor/stemmer/FrequencyTrie::metadataForCurrentStream → KILLED |
return new TrieMetadata(STREAM_VERSION, metadata.traversalDirection(), metadata.reductionSettings(), |
| 1027 | metadata.diacriticProcessingMode(), metadata.caseProcessingMode()); | |
| 1028 | } | |
| 1029 | ||
| 1030 | /** | |
| 1031 | * Returns the number of canonical compiled nodes reachable from the root. | |
| 1032 | * | |
| 1033 | * <p> | |
| 1034 | * The returned value reflects the size of the final reduced immutable trie, not | |
| 1035 | * the number of mutable build-time nodes inserted before reduction. Shared | |
| 1036 | * canonical subtrees are counted only once. | |
| 1037 | * | |
| 1038 | * @return number of canonical compiled nodes in this trie | |
| 1039 | */ | |
| 1040 | public int size() { | |
| 1041 | final Map<CompiledNode<V>, Integer> nodeIds = new IdentityHashMap<>(); | |
| 1042 | final List<CompiledNode<V>> orderedNodes = new ArrayList<>(); | |
| 1043 |
1
1. size : removed call to org/egothor/stemmer/FrequencyTrie::assignNodeIds → KILLED |
assignNodeIds(this.root, nodeIds, orderedNodes); |
| 1044 |
1
1. size : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::size → KILLED |
return orderedNodes.size(); |
| 1045 | } | |
| 1046 | ||
| 1047 | /** | |
| 1048 | * Assigns deterministic identifiers to all canonical compiled nodes reachable | |
| 1049 | * from the supplied root. | |
| 1050 | * | |
| 1051 | * @param node current node | |
| 1052 | * @param nodeIds assigned node identifiers | |
| 1053 | * @param orderedNodes ordered nodes in identifier order | |
| 1054 | */ | |
| 1055 | private static <V> void assignNodeIds(final CompiledNode<V> node, final Map<CompiledNode<V>, Integer> nodeIds, | |
| 1056 | final List<CompiledNode<V>> orderedNodes) { | |
| 1057 |
1
1. assignNodeIds : negated conditional → KILLED |
if (nodeIds.containsKey(node)) { |
| 1058 | return; | |
| 1059 | } | |
| 1060 | ||
| 1061 | final int nodeId = orderedNodes.size(); | |
| 1062 | nodeIds.put(node, nodeId); | |
| 1063 | orderedNodes.add(node); | |
| 1064 | ||
| 1065 | for (CompiledNode<V> child : node.children()) { | |
| 1066 |
1
1. assignNodeIds : removed call to org/egothor/stemmer/FrequencyTrie::assignNodeIds → KILLED |
assignNodeIds(child, nodeIds, orderedNodes); |
| 1067 | } | |
| 1068 | } | |
| 1069 | ||
| 1070 | /** | |
| 1071 | * Collects the deterministic equality-based value table for serialization. | |
| 1072 | * | |
| 1073 | * <p> | |
| 1074 | * Nodes are visited in canonical node-identifier order and values are visited | |
| 1075 | * in their existing node-local order. The first occurrence according to | |
| 1076 | * {@link Object#equals(Object)} and {@link Object#hashCode()} assigns the table | |
| 1077 | * index. | |
| 1078 | * </p> | |
| 1079 | * | |
| 1080 | * @param orderedNodes canonical nodes in identifier order | |
| 1081 | * @param valueIds destination mapping from values to table indexes | |
| 1082 | * @param distinctValues destination values in table-index order | |
| 1083 | * @param <V> value type | |
| 1084 | */ | |
| 1085 | private static <V> void collectDistinctValues(final List<CompiledNode<V>> orderedNodes, | |
| 1086 | final Map<V, Integer> valueIds, final List<V> distinctValues) { | |
| 1087 | for (CompiledNode<V> node : orderedNodes) { | |
| 1088 | for (V value : node.orderedValues()) { | |
| 1089 |
1
1. collectDistinctValues : negated conditional → KILLED |
if (!valueIds.containsKey(value)) { |
| 1090 | final int valueId = distinctValues.size(); | |
| 1091 | valueIds.put(value, valueId); | |
| 1092 | distinctValues.add(value); | |
| 1093 | } | |
| 1094 | } | |
| 1095 | } | |
| 1096 | } | |
| 1097 | ||
| 1098 | /** | |
| 1099 | * Writes every distinct value exactly once in table-index order. | |
| 1100 | * | |
| 1101 | * @param dataOutput output stream | |
| 1102 | * @param valueCodec codec responsible for value encoding | |
| 1103 | * @param distinctValues distinct values in deterministic table order | |
| 1104 | * @param <V> value type | |
| 1105 | * @throws IOException if writing the table fails | |
| 1106 | */ | |
| 1107 | private static <V> void writeValueTable(final DataOutputStream dataOutput, final ValueStreamCodec<V> valueCodec, | |
| 1108 | final List<V> distinctValues) throws IOException { | |
| 1109 |
1
1. writeValueTable : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(distinctValues.size()); |
| 1110 | for (V value : distinctValues) { | |
| 1111 |
1
1. writeValueTable : removed call to org/egothor/stemmer/FrequencyTrie$ValueStreamCodec::write → KILLED |
valueCodec.write(dataOutput, value); |
| 1112 | } | |
| 1113 | } | |
| 1114 | ||
| 1115 | /** | |
| 1116 | * Writes one compiled node using stream-local value-table indexes. | |
| 1117 | * | |
| 1118 | * @param dataOutput output | |
| 1119 | * @param node node to write | |
| 1120 | * @param nodeId canonical identifier of {@code node} | |
| 1121 | * @param nodeIds node identifiers | |
| 1122 | * @param valueIds value-table indexes | |
| 1123 | * @param <V> value type | |
| 1124 | * @throws IOException if writing fails | |
| 1125 | */ | |
| 1126 | private static <V> void writeNode(final DataOutputStream dataOutput, final CompiledNode<V> node, final int nodeId, | |
| 1127 | final Map<CompiledNode<V>, Integer> nodeIds, final Map<V, Integer> valueIds) throws IOException { | |
| 1128 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeBoolean → KILLED |
dataOutput.writeBoolean(node.acceptsRemainingInput()); |
| 1129 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(node.edgeLabels().length); |
| 1130 |
2
1. writeNode : changed conditional boundary → KILLED 2. writeNode : negated conditional → KILLED |
for (int index = 0; index < node.edgeLabels().length; index++) { |
| 1131 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeChar → KILLED |
dataOutput.writeChar(node.edgeLabels()[index]); |
| 1132 | final Integer childNodeId = nodeIds.get(node.children()[index]); | |
| 1133 |
1
1. writeNode : negated conditional → KILLED |
if (childNodeId == null) { |
| 1134 | throw new IOException("Missing child node identifier during serialization."); | |
| 1135 | } | |
| 1136 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(childNodeId); |
| 1137 | } | |
| 1138 | ||
| 1139 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(node.orderedValues().length); |
| 1140 |
2
1. writeNode : changed conditional boundary → KILLED 2. writeNode : negated conditional → KILLED |
for (int index = 0; index < node.orderedValues().length; index++) { |
| 1141 | final V value = node.orderedValues()[index]; | |
| 1142 | final Integer valueId = valueIds.get(value); | |
| 1143 |
1
1. writeNode : negated conditional → KILLED |
if (valueId == null) { |
| 1144 |
1
1. writeNode : negated conditional → NO_COVERAGE |
final String valueContext = value == null ? "null" |
| 1145 | : value.getClass().getName() + '[' + value + ']'; | |
| 1146 | throw new IOException("Missing value table index at canonical node " + nodeId + ", local value " | |
| 1147 | + index + ": " + valueContext); | |
| 1148 | } | |
| 1149 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(valueId); |
| 1150 |
1
1. writeNode : removed call to java/io/DataOutputStream::writeInt → KILLED |
dataOutput.writeInt(node.orderedCounts()[index]); |
| 1151 | } | |
| 1152 | } | |
| 1153 | ||
| 1154 | private static MessageDigest newSha256Digest() { | |
| 1155 | try { | |
| 1156 |
1
1. newSha256Digest : replaced return value with null for org/egothor/stemmer/FrequencyTrie::newSha256Digest → KILLED |
return MessageDigest.getInstance("SHA-256"); |
| 1157 | } catch (NoSuchAlgorithmException exception) { | |
| 1158 | throw new IllegalStateException("SHA-256 digest is not available.", exception); | |
| 1159 | } | |
| 1160 | } | |
| 1161 | ||
| 1162 | private static <V> void updateNodeFingerprint(final MessageDigest messageDigest, final CompiledNode<V> node, | |
| 1163 | final Map<CompiledNode<V>, Integer> nodeIds) { | |
| 1164 | final char[] edgeLabels = node.edgeLabels(); | |
| 1165 | final CompiledNode<V>[] children = node.children(); | |
| 1166 | final V[] values = node.orderedValues(); | |
| 1167 | final int[] counts = node.orderedCounts(); | |
| 1168 | ||
| 1169 |
2
1. updateNodeFingerprint : negated conditional → SURVIVED 2. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, node.acceptsRemainingInput() ? 1 : 0); |
| 1170 |
1
1. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, edgeLabels.length); |
| 1171 | for (char edgeLabel : edgeLabels) { | |
| 1172 |
1
1. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, edgeLabel); |
| 1173 | } | |
| 1174 | for (CompiledNode<V> child : children) { | |
| 1175 | final Integer childNodeId = nodeIds.get(child); | |
| 1176 |
1
1. updateNodeFingerprint : negated conditional → KILLED |
if (childNodeId == null) { |
| 1177 | throw new IllegalStateException("Missing child node identifier during trie fingerprinting."); | |
| 1178 | } | |
| 1179 |
1
1. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, childNodeId); |
| 1180 | } | |
| 1181 | ||
| 1182 |
1
1. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, values.length); |
| 1183 | for (V value : values) { | |
| 1184 |
1
1. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateUtf8 → SURVIVED |
updateUtf8(messageDigest, String.valueOf(value)); |
| 1185 | } | |
| 1186 | for (int count : counts) { | |
| 1187 |
1
1. updateNodeFingerprint : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → KILLED |
updateInt(messageDigest, count); |
| 1188 | } | |
| 1189 | } | |
| 1190 | ||
| 1191 | private static void updateUtf8(final MessageDigest messageDigest, final String value) { | |
| 1192 | final byte[] encoded = value.getBytes(StandardCharsets.UTF_8); | |
| 1193 |
1
1. updateUtf8 : removed call to org/egothor/stemmer/FrequencyTrie::updateInt → SURVIVED |
updateInt(messageDigest, encoded.length); |
| 1194 |
1
1. updateUtf8 : removed call to java/security/MessageDigest::update → SURVIVED |
messageDigest.update(encoded); |
| 1195 | } | |
| 1196 | ||
| 1197 | private static void updateInt(final MessageDigest messageDigest, final int value) { | |
| 1198 |
2
1. updateInt : removed call to java/security/MessageDigest::update → SURVIVED 2. updateInt : Replaced Unsigned Shift Right with Shift Left → SURVIVED |
messageDigest.update((byte) (value >>> 24)); |
| 1199 |
2
1. updateInt : removed call to java/security/MessageDigest::update → SURVIVED 2. updateInt : Replaced Unsigned Shift Right with Shift Left → SURVIVED |
messageDigest.update((byte) (value >>> 16)); |
| 1200 |
2
1. updateInt : removed call to java/security/MessageDigest::update → SURVIVED 2. updateInt : Replaced Unsigned Shift Right with Shift Left → SURVIVED |
messageDigest.update((byte) (value >>> 8)); |
| 1201 |
1
1. updateInt : removed call to java/security/MessageDigest::update → KILLED |
messageDigest.update((byte) value); |
| 1202 | } | |
| 1203 | ||
| 1204 | private static String toLowerHex(final byte[] digest) { | |
| 1205 |
1
1. toLowerHex : Replaced integer multiplication with division → SURVIVED |
final StringBuilder builder = new StringBuilder(digest.length * 2); |
| 1206 | for (byte item : digest) { | |
| 1207 |
3
1. toLowerHex : Replaced Unsigned Shift Right with Shift Left → KILLED 2. toLowerHex : Replaced bitwise AND with OR → KILLED 3. toLowerHex : Replaced bitwise AND with OR → KILLED |
builder.append(Character.forDigit((item >>> 4) & 0x0F, 16)).append(Character.forDigit(item & 0x0F, 16)); |
| 1208 | } | |
| 1209 |
1
1. toLowerHex : replaced return value with "" for org/egothor/stemmer/FrequencyTrie::toLowerHex → KILLED |
return builder.toString(); |
| 1210 | } | |
| 1211 | ||
| 1212 | /** | |
| 1213 | * Internal helper that materializes serialized trie data. | |
| 1214 | * | |
| 1215 | * <p> | |
| 1216 | * Moving reader complexity into this helper keeps the public-facing class from | |
| 1217 | * accumulating excessive class-level cyclomatic complexity while preserving the | |
| 1218 | * same binary compatibility contract. | |
| 1219 | * </p> | |
| 1220 | */ | |
| 1221 | private static final class CompiledTrieReader { | |
| 1222 | ||
| 1223 | private static <V> FrequencyTrie<V> read(final InputStream inputStream, final IntFunction<V[]> arrayFactory, | |
| 1224 | final MetadataValueStreamReader<V> valueReader, final int maxExpandedIndex) throws IOException { | |
| 1225 | Objects.requireNonNull(inputStream, "inputStream"); | |
| 1226 | Objects.requireNonNull(arrayFactory, "arrayFactory"); | |
| 1227 | Objects.requireNonNull(valueReader, "valueReader"); | |
| 1228 |
2
1. read : changed conditional boundary → KILLED 2. read : negated conditional → KILLED |
if (maxExpandedIndex < -1) { |
| 1229 | throw new IllegalArgumentException("maxExpandedIndex must be >= -1."); | |
| 1230 | } | |
| 1231 | ||
| 1232 | final DataInputStream dataInput = wrapInputStream(inputStream); | |
| 1233 | final int magic = dataInput.readInt(); | |
| 1234 |
1
1. read : negated conditional → KILLED |
if (magic != STREAM_MAGIC) { |
| 1235 | throw new IOException("Unsupported trie stream header: " + Integer.toHexString(magic)); | |
| 1236 | } | |
| 1237 | ||
| 1238 | final int version = dataInput.readInt(); | |
| 1239 |
4
1. read : negated conditional → KILLED 2. read : changed conditional boundary → KILLED 3. read : changed conditional boundary → KILLED 4. read : negated conditional → KILLED |
if (version < MIN_STREAM_VERSION || version > STREAM_VERSION) { |
| 1240 | throw new IOException("Unsupported trie stream version: " + version); | |
| 1241 | } | |
| 1242 | ||
| 1243 | final int nodeCount = dataInput.readInt(); | |
| 1244 |
2
1. read : changed conditional boundary → SURVIVED 2. read : negated conditional → KILLED |
if (nodeCount < 0) { |
| 1245 | throw new IOException("Negative node count: " + nodeCount); | |
| 1246 | } | |
| 1247 | ||
| 1248 | final int rootNodeId = dataInput.readInt(); | |
| 1249 |
4
1. read : changed conditional boundary → KILLED 2. read : changed conditional boundary → KILLED 3. read : negated conditional → KILLED 4. read : negated conditional → KILLED |
if (rootNodeId < 0 || rootNodeId >= nodeCount) { |
| 1250 | throw new IOException("Invalid root node id: " + rootNodeId); | |
| 1251 | } | |
| 1252 | ||
| 1253 | final TrieMetadata sourceMetadata = readMetadata(dataInput, version); | |
| 1254 |
2
1. read : negated conditional → KILLED 2. read : changed conditional boundary → KILLED |
final V[] valueTable = version >= VALUE_TABLE_VERSION |
| 1255 | ? readValueTable(dataInput, arrayFactory, valueReader, sourceMetadata) | |
| 1256 | : null; | |
| 1257 |
2
1. read : negated conditional → KILLED 2. read : changed conditional boundary → KILLED |
final int effectiveMaxExpandedIndex = maxExpandedIndex >= 0 ? maxExpandedIndex : DEFAULT_MAX_EXPANDED_INDEX; |
| 1258 | final CompiledNode<V>[] nodes = readNodes(dataInput, arrayFactory, valueReader, sourceMetadata, valueTable, | |
| 1259 | nodeCount, effectiveMaxExpandedIndex, version); | |
| 1260 | final CompiledNode<V> rootNode = nodes[rootNodeId]; | |
| 1261 | ||
| 1262 | if (LOGGER.isLoggable(Level.FINE)) { | |
| 1263 | LOGGER.log(Level.FINE, "Read compiled trie with {0} canonical nodes.", nodeCount); | |
| 1264 | } | |
| 1265 | ||
| 1266 |
1
1. read : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::read → KILLED |
return new FrequencyTrie<>(arrayFactory, rootNode, sourceMetadata); |
| 1267 | } | |
| 1268 | ||
| 1269 | /** | |
| 1270 | * Reads the temporary stream-local value table. | |
| 1271 | * | |
| 1272 | * <p> | |
| 1273 | * Each serialized value is decoded exactly once. The returned array is used | |
| 1274 | * only while materializing node value arrays and is not retained by the | |
| 1275 | * resulting trie. | |
| 1276 | * </p> | |
| 1277 | * | |
| 1278 | * @param dataInput input stream | |
| 1279 | * @param arrayFactory typed-array factory | |
| 1280 | * @param valueReader metadata-aware value reader | |
| 1281 | * @param metadata parsed trie metadata | |
| 1282 | * @param <V> value type | |
| 1283 | * @return decoded values in table-index order | |
| 1284 | * @throws IOException if the count is negative or value decoding fails | |
| 1285 | */ | |
| 1286 | private static <V> V[] readValueTable(final DataInputStream dataInput, final IntFunction<V[]> arrayFactory, | |
| 1287 | final MetadataValueStreamReader<V> valueReader, final TrieMetadata metadata) throws IOException { | |
| 1288 | final int distinctValueCount = dataInput.readInt(); | |
| 1289 |
2
1. readValueTable : changed conditional boundary → SURVIVED 2. readValueTable : negated conditional → KILLED |
if (distinctValueCount < 0) { |
| 1290 | throw new IOException("Negative distinct value count: " + distinctValueCount); | |
| 1291 | } | |
| 1292 | ||
| 1293 | final V[] valueTable = arrayFactory.apply(distinctValueCount); | |
| 1294 |
2
1. readValueTable : negated conditional → KILLED 2. readValueTable : changed conditional boundary → KILLED |
for (int valueIndex = 0; valueIndex < distinctValueCount; valueIndex++) { |
| 1295 | valueTable[valueIndex] = valueReader.read(dataInput, metadata); | |
| 1296 | } | |
| 1297 |
1
1. readValueTable : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readValueTable → KILLED |
return valueTable; |
| 1298 | } | |
| 1299 | ||
| 1300 | private static DataInputStream wrapInputStream(final InputStream inputStream) { | |
| 1301 |
2
1. wrapInputStream : negated conditional → KILLED 2. wrapInputStream : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::wrapInputStream → KILLED |
return inputStream instanceof DataInputStream ? (DataInputStream) inputStream |
| 1302 | : new DataInputStream(inputStream); | |
| 1303 | } | |
| 1304 | ||
| 1305 | private static TrieMetadata readMetadata(final DataInputStream dataInput, final int version) | |
| 1306 | throws IOException { | |
| 1307 |
2
1. readMetadata : negated conditional → KILLED 2. readMetadata : changed conditional boundary → KILLED |
if (version >= TEXT_METADATA_VERSION) { |
| 1308 |
1
1. readMetadata : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readMetadata → KILLED |
return readTextMetadata(dataInput, version); |
| 1309 | } | |
| 1310 | ||
| 1311 | final WordTraversalDirection traversalDirection = readTraversalDirection(dataInput, version); | |
| 1312 |
2
1. readMetadata : negated conditional → KILLED 2. readMetadata : changed conditional boundary → KILLED |
if (version < REDUCTION_VERSION) { |
| 1313 |
1
1. readMetadata : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readMetadata → KILLED |
return TrieMetadata.legacy(version, traversalDirection); |
| 1314 | } | |
| 1315 | ||
| 1316 | final ReductionSettings reductionSettings = readReductionSettings(dataInput); | |
| 1317 | final DiacriticProcessingMode diacriticProcessingMode = readEnumByOrdinal(dataInput, | |
| 1318 | DiacriticProcessingMode.values(), "diacritic processing mode"); | |
| 1319 |
2
1. readMetadata : negated conditional → KILLED 2. readMetadata : changed conditional boundary → KILLED |
final CaseProcessingMode caseProcessingMode = version >= CASE_VERSION ? readCaseProcessingMode(dataInput) |
| 1320 | : CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT; | |
| 1321 |
1
1. readMetadata : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readMetadata → KILLED |
return new TrieMetadata(version, traversalDirection, reductionSettings, diacriticProcessingMode, |
| 1322 | caseProcessingMode); | |
| 1323 | } | |
| 1324 | ||
| 1325 | private static TrieMetadata readTextMetadata(final DataInputStream dataInput, final int version) | |
| 1326 | throws IOException { | |
| 1327 | try { | |
| 1328 |
1
1. readTextMetadata : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readTextMetadata → KILLED |
return TrieMetadata.fromTextBlock(version, dataInput.readUTF()); |
| 1329 | } catch (IllegalArgumentException exception) { | |
| 1330 | throw new IOException("Invalid metadata block.", exception); | |
| 1331 | } | |
| 1332 | } | |
| 1333 | ||
| 1334 | private static WordTraversalDirection readTraversalDirection(final DataInputStream dataInput, final int version) | |
| 1335 | throws IOException { | |
| 1336 |
2
1. readTraversalDirection : negated conditional → KILLED 2. readTraversalDirection : changed conditional boundary → KILLED |
if (version < TRAVERSAL_VERSION) { |
| 1337 |
1
1. readTraversalDirection : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readTraversalDirection → KILLED |
return WordTraversalDirection.BACKWARD; |
| 1338 | } | |
| 1339 |
1
1. readTraversalDirection : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readTraversalDirection → KILLED |
return readEnumByOrdinal(dataInput, WordTraversalDirection.values(), "traversal direction"); |
| 1340 | } | |
| 1341 | ||
| 1342 | private static ReductionSettings readReductionSettings(final DataInputStream dataInput) throws IOException { | |
| 1343 | final ReductionMode reductionMode = readEnumByOrdinal(dataInput, ReductionMode.values(), "reduction mode"); | |
| 1344 | final int dominantWinnerMinPercent = dataInput.readInt(); | |
| 1345 | final int dominantWinnerOverSecondRatio = dataInput.readInt(); // NOPMD | |
| 1346 |
1
1. readReductionSettings : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readReductionSettings → KILLED |
return new ReductionSettings(reductionMode, dominantWinnerMinPercent, dominantWinnerOverSecondRatio); |
| 1347 | } | |
| 1348 | ||
| 1349 | private static CaseProcessingMode readCaseProcessingMode(final DataInputStream dataInput) throws IOException { | |
| 1350 |
1
1. readCaseProcessingMode : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readCaseProcessingMode → KILLED |
return readEnumByOrdinal(dataInput, CaseProcessingMode.values(), "case processing mode"); |
| 1351 | } | |
| 1352 | ||
| 1353 | private static <E extends Enum<E>> E readEnumByOrdinal(final DataInputStream dataInput, final E[] values, | |
| 1354 | final String name) throws IOException { | |
| 1355 | final int ordinal = dataInput.readInt(); | |
| 1356 |
4
1. readEnumByOrdinal : changed conditional boundary → SURVIVED 2. readEnumByOrdinal : negated conditional → KILLED 3. readEnumByOrdinal : changed conditional boundary → KILLED 4. readEnumByOrdinal : negated conditional → KILLED |
if (ordinal < 0 || ordinal >= values.length) { |
| 1357 | throw new IOException("Invalid " + name + " ordinal: " + ordinal); | |
| 1358 | } | |
| 1359 |
1
1. readEnumByOrdinal : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readEnumByOrdinal → KILLED |
return values[ordinal]; |
| 1360 | } | |
| 1361 | ||
| 1362 | private static <V> CompiledNode<V>[] readNodes(final DataInputStream dataInput, | |
| 1363 | final IntFunction<V[]> arrayFactory, final MetadataValueStreamReader<V> valueReader, | |
| 1364 | final TrieMetadata metadata, final V[] valueTable, final int nodeCount, final int maxExpandedIndex, | |
| 1365 | final int version) throws IOException { | |
| 1366 | final char[][] edgeLabelsByNode = new char[nodeCount][]; | |
| 1367 | final int[][] childNodeIdsByNode = new int[nodeCount][]; | |
| 1368 | @SuppressWarnings("unchecked") | |
| 1369 | final V[][] orderedValuesByNode = (V[][]) new Object[nodeCount][]; | |
| 1370 | final int[][] orderedCountsByNode = new int[nodeCount][]; | |
| 1371 | final boolean[] acceptsRemainingInputByNode = new boolean[nodeCount]; | |
| 1372 | ||
| 1373 |
2
1. readNodes : changed conditional boundary → KILLED 2. readNodes : negated conditional → KILLED |
for (int nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) { |
| 1374 |
2
1. readNodes : negated conditional → KILLED 2. readNodes : changed conditional boundary → KILLED |
if (version >= ACCEPTING_NODE_VERSION) { |
| 1375 | acceptsRemainingInputByNode[nodeIndex] = dataInput.readBoolean(); | |
| 1376 | } | |
| 1377 | ||
| 1378 | final int edgeCount = dataInput.readInt(); | |
| 1379 |
2
1. readNodes : negated conditional → KILLED 2. readNodes : changed conditional boundary → KILLED |
if (edgeCount < 0) { |
| 1380 | throw new IOException("Negative edge count at node " + nodeIndex + ": " + edgeCount); | |
| 1381 | } | |
| 1382 | ||
| 1383 | edgeLabelsByNode[nodeIndex] = new char[edgeCount]; | |
| 1384 | childNodeIdsByNode[nodeIndex] = new int[edgeCount]; | |
| 1385 | ||
| 1386 |
2
1. readNodes : negated conditional → KILLED 2. readNodes : changed conditional boundary → KILLED |
for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) { |
| 1387 | edgeLabelsByNode[nodeIndex][edgeIndex] = dataInput.readChar(); | |
| 1388 | childNodeIdsByNode[nodeIndex][edgeIndex] = dataInput.readInt(); | |
| 1389 | } | |
| 1390 | ||
| 1391 |
1
1. readNodes : removed call to org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::validateSerializedEdges → KILLED |
validateSerializedEdges(nodeIndex, edgeLabelsByNode[nodeIndex]); |
| 1392 | ||
| 1393 | final int valueCount = dataInput.readInt(); | |
| 1394 |
2
1. readNodes : changed conditional boundary → KILLED 2. readNodes : negated conditional → KILLED |
if (valueCount < 0) { |
| 1395 | throw new IOException("Negative value count at node " + nodeIndex + ": " + valueCount); | |
| 1396 | } | |
| 1397 | // An accepting node may also carry child edges: under LookupMode.FIRST | |
| 1398 | // the accept short-circuits descent (children are inert), while | |
| 1399 | // LookupMode.LAST/ALL follow the deeper edges. Such nodes arise when a | |
| 1400 | // custom pair is added through a contracted generalization (see | |
| 1401 | // FrequencyTrieBuilders / the Python TrieBuilder). | |
| 1402 |
2
1. readNodes : negated conditional → KILLED 2. readNodes : negated conditional → KILLED |
if (acceptsRemainingInputByNode[nodeIndex] && valueCount == 0) { |
| 1403 | throw new IOException("Accepting node " + nodeIndex + " must store at least one value."); | |
| 1404 | } | |
| 1405 | ||
| 1406 | orderedValuesByNode[nodeIndex] = arrayFactory.apply(valueCount); | |
| 1407 | orderedCountsByNode[nodeIndex] = new int[valueCount]; | |
| 1408 | ||
| 1409 |
2
1. readNodes : changed conditional boundary → KILLED 2. readNodes : negated conditional → KILLED |
for (int valueIndex = 0; valueIndex < valueCount; valueIndex++) { |
| 1410 |
2
1. readNodes : negated conditional → KILLED 2. readNodes : changed conditional boundary → KILLED |
if (version >= VALUE_TABLE_VERSION) { |
| 1411 | final int valueTableIndex = dataInput.readInt(); | |
| 1412 |
4
1. readNodes : changed conditional boundary → KILLED 2. readNodes : changed conditional boundary → KILLED 3. readNodes : negated conditional → KILLED 4. readNodes : negated conditional → KILLED |
if (valueTableIndex < 0 || valueTableIndex >= valueTable.length) { |
| 1413 | throw new IOException("Invalid value table index at node " + nodeIndex + ", local value " | |
| 1414 | + valueIndex + ": " + valueTableIndex + "; table size is " + valueTable.length | |
| 1415 | + '.'); | |
| 1416 | } | |
| 1417 | orderedValuesByNode[nodeIndex][valueIndex] = valueTable[valueTableIndex]; | |
| 1418 | } else { | |
| 1419 | orderedValuesByNode[nodeIndex][valueIndex] = valueReader.read(dataInput, metadata); | |
| 1420 | } | |
| 1421 | orderedCountsByNode[nodeIndex][valueIndex] = dataInput.readInt(); | |
| 1422 |
2
1. readNodes : negated conditional → KILLED 2. readNodes : changed conditional boundary → KILLED |
if (orderedCountsByNode[nodeIndex][valueIndex] <= 0) { |
| 1423 | throw new IOException("Non-positive stored count at node " + nodeIndex + ", value index " | |
| 1424 | + valueIndex + ": " + orderedCountsByNode[nodeIndex][valueIndex]); | |
| 1425 | } | |
| 1426 | } | |
| 1427 | } | |
| 1428 | ||
| 1429 | @SuppressWarnings("unchecked") | |
| 1430 | final CompiledNode<V>[] nodes = new CompiledNode[nodeCount]; | |
| 1431 | final boolean[] inProgress = new boolean[nodeCount]; | |
| 1432 | ||
| 1433 |
2
1. readNodes : changed conditional boundary → KILLED 2. readNodes : negated conditional → KILLED |
for (int nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) { |
| 1434 | nodes[nodeIndex] = resolveNode(nodeIndex, edgeLabelsByNode, childNodeIdsByNode, orderedValuesByNode, | |
| 1435 | orderedCountsByNode, acceptsRemainingInputByNode, nodes, inProgress, maxExpandedIndex); | |
| 1436 | } | |
| 1437 | ||
| 1438 |
1
1. readNodes : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::readNodes → KILLED |
return nodes; |
| 1439 | } | |
| 1440 | ||
| 1441 | private static <V> CompiledNode<V> resolveNode(final int nodeIndex, final char[][] edgeLabelsByNode, | |
| 1442 | final int[][] childNodeIdsByNode, final V[][] orderedValuesByNode, final int[][] orderedCountsByNode, | |
| 1443 | final boolean[] acceptsRemainingInputByNode, final CompiledNode<V>[] nodes, | |
| 1444 | final boolean[] inProgress, final int maxExpandedIndex) throws IOException { | |
| 1445 | final CompiledNode<V> cachedNode = nodes[nodeIndex]; | |
| 1446 |
1
1. resolveNode : negated conditional → KILLED |
if (cachedNode != null) { |
| 1447 |
1
1. resolveNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::resolveNode → KILLED |
return cachedNode; |
| 1448 | } | |
| 1449 | ||
| 1450 |
1
1. resolveNode : negated conditional → KILLED |
if (inProgress[nodeIndex]) { |
| 1451 | throw new IOException( | |
| 1452 | "Invalid serialized node graph: cyclic reference detected at node " + nodeIndex + '.'); | |
| 1453 | } | |
| 1454 | inProgress[nodeIndex] = true; | |
| 1455 | try { | |
| 1456 | final char[] edgeLabels = edgeLabelsByNode[nodeIndex]; | |
| 1457 | final int[] childNodeIds = childNodeIdsByNode[nodeIndex]; | |
| 1458 | final int edgeCount = childNodeIds.length; | |
| 1459 | @SuppressWarnings("unchecked") | |
| 1460 | final CompiledNode<V>[] children = new CompiledNode[edgeCount]; | |
| 1461 | ||
| 1462 |
2
1. resolveNode : negated conditional → KILLED 2. resolveNode : changed conditional boundary → KILLED |
for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) { |
| 1463 | final int childNodeId = childNodeIds[edgeIndex]; | |
| 1464 |
4
1. resolveNode : changed conditional boundary → SURVIVED 2. resolveNode : negated conditional → KILLED 3. resolveNode : changed conditional boundary → KILLED 4. resolveNode : negated conditional → KILLED |
if (childNodeId < 0 || childNodeId >= edgeLabelsByNode.length) { |
| 1465 | throw new IOException("Invalid child node id at node " + nodeIndex + ", edge index " + edgeIndex | |
| 1466 | + ": " + childNodeId); | |
| 1467 | } | |
| 1468 | children[edgeIndex] = resolveNode(childNodeId, edgeLabelsByNode, childNodeIdsByNode, | |
| 1469 | orderedValuesByNode, orderedCountsByNode, acceptsRemainingInputByNode, nodes, inProgress, | |
| 1470 | maxExpandedIndex); | |
| 1471 | } | |
| 1472 | ||
| 1473 | final CompiledNode<V> node = new CompiledNode<>(edgeLabels, children, orderedValuesByNode[nodeIndex], | |
| 1474 | acceptsRemainingInputByNode[nodeIndex], maxExpandedIndex, orderedCountsByNode[nodeIndex]); | |
| 1475 | nodes[nodeIndex] = node; | |
| 1476 |
1
1. resolveNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie$CompiledTrieReader::resolveNode → KILLED |
return node; |
| 1477 | } finally { | |
| 1478 | inProgress[nodeIndex] = false; | |
| 1479 | } | |
| 1480 | } | |
| 1481 | ||
| 1482 | private static void validateSerializedEdges(final int nodeIndex, final char... edgeLabels) throws IOException { | |
| 1483 |
2
1. validateSerializedEdges : changed conditional boundary → KILLED 2. validateSerializedEdges : negated conditional → KILLED |
for (int edgeIndex = 1; edgeIndex < edgeLabels.length; edgeIndex++) { |
| 1484 |
3
1. validateSerializedEdges : changed conditional boundary → SURVIVED 2. validateSerializedEdges : negated conditional → KILLED 3. validateSerializedEdges : Replaced integer subtraction with addition → KILLED |
if (edgeLabels[edgeIndex - 1] >= edgeLabels[edgeIndex]) { |
| 1485 |
1
1. validateSerializedEdges : Replaced integer subtraction with addition → KILLED |
throw new IOException( |
| 1486 | "Edge labels must be strictly ascending at node " + nodeIndex + ", edge index " + edgeIndex | |
| 1487 | + ": '" + edgeLabels[edgeIndex - 1] + "' then '" + edgeLabels[edgeIndex] + "'."); | |
| 1488 | } | |
| 1489 | } | |
| 1490 | } | |
| 1491 | } | |
| 1492 | ||
| 1493 | /** | |
| 1494 | * Locates the compiled node for the supplied key. | |
| 1495 | * | |
| 1496 | * @param key already-normalized key to resolve | |
| 1497 | * @return compiled node, or {@code null} if the path does not exist | |
| 1498 | */ | |
| 1499 | private CompiledNode<V> findNode(final String key) { | |
| 1500 | CompiledNode<V> current = this.root; | |
| 1501 |
1
1. findNode : negated conditional → KILLED |
if (this.lookupTraversalDirection == WordTraversalDirection.BACKWARD) { |
| 1502 |
3
1. findNode : changed conditional boundary → KILLED 2. findNode : Replaced integer subtraction with addition → KILLED 3. findNode : negated conditional → KILLED |
for (int traversalOffset = key.length() - 1; traversalOffset >= 0; traversalOffset--) { |
| 1503 |
1
1. findNode : negated conditional → KILLED |
if (current.acceptsRemainingInput()) { |
| 1504 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → KILLED |
return current; |
| 1505 | } | |
| 1506 | current = current.findChild(key.charAt(traversalOffset)); | |
| 1507 |
1
1. findNode : negated conditional → KILLED |
if (current == null) { |
| 1508 | return null; | |
| 1509 | } | |
| 1510 | } | |
| 1511 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → KILLED |
return current; |
| 1512 | } | |
| 1513 | ||
| 1514 |
2
1. findNode : negated conditional → KILLED 2. findNode : changed conditional boundary → KILLED |
for (int traversalOffset = 0; traversalOffset < key.length(); traversalOffset++) { |
| 1515 |
1
1. findNode : negated conditional → KILLED |
if (current.acceptsRemainingInput()) { |
| 1516 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → KILLED |
return current; |
| 1517 | } | |
| 1518 | current = current.findChild(key.charAt(traversalOffset)); | |
| 1519 |
1
1. findNode : negated conditional → KILLED |
if (current == null) { |
| 1520 | return null; | |
| 1521 | } | |
| 1522 | } | |
| 1523 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → KILLED |
return current; |
| 1524 | } | |
| 1525 | ||
| 1526 | /** | |
| 1527 | * Locates the compiled node for the supplied key. | |
| 1528 | * | |
| 1529 | * @param key already-normalized key to resolve | |
| 1530 | * @return compiled node, or {@code null} if the path does not exist | |
| 1531 | */ | |
| 1532 | private CompiledNode<V> findNode(final CharSequence key) { | |
| 1533 | CompiledNode<V> current = this.root; | |
| 1534 |
1
1. findNode : negated conditional → KILLED |
if (this.lookupTraversalDirection == WordTraversalDirection.BACKWARD) { |
| 1535 |
3
1. findNode : changed conditional boundary → KILLED 2. findNode : Replaced integer subtraction with addition → KILLED 3. findNode : negated conditional → KILLED |
for (int traversalOffset = key.length() - 1; traversalOffset >= 0; traversalOffset--) { |
| 1536 |
1
1. findNode : negated conditional → KILLED |
if (current.acceptsRemainingInput()) { |
| 1537 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → NO_COVERAGE |
return current; |
| 1538 | } | |
| 1539 | current = current.findChild(key.charAt(traversalOffset)); | |
| 1540 |
1
1. findNode : negated conditional → KILLED |
if (current == null) { |
| 1541 | return null; | |
| 1542 | } | |
| 1543 | } | |
| 1544 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → KILLED |
return current; |
| 1545 | } | |
| 1546 | ||
| 1547 |
2
1. findNode : negated conditional → NO_COVERAGE 2. findNode : changed conditional boundary → NO_COVERAGE |
for (int traversalOffset = 0; traversalOffset < key.length(); traversalOffset++) { |
| 1548 |
1
1. findNode : negated conditional → NO_COVERAGE |
if (current.acceptsRemainingInput()) { |
| 1549 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → NO_COVERAGE |
return current; |
| 1550 | } | |
| 1551 | current = current.findChild(key.charAt(traversalOffset)); | |
| 1552 |
1
1. findNode : negated conditional → NO_COVERAGE |
if (current == null) { |
| 1553 | return null; | |
| 1554 | } | |
| 1555 | } | |
| 1556 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → NO_COVERAGE |
return current; |
| 1557 | } | |
| 1558 | ||
| 1559 | /** | |
| 1560 | * Locates the compiled node for the supplied key slice. | |
| 1561 | * | |
| 1562 | * @param key already-normalized key storage | |
| 1563 | * @param offset first character offset | |
| 1564 | * @param length number of characters to read | |
| 1565 | * @return compiled node, or {@code null} if the path does not exist | |
| 1566 | */ | |
| 1567 | private CompiledNode<V> findNode(final char[] key, final int offset, final int length) { | |
| 1568 | CompiledNode<V> current = this.root; | |
| 1569 |
1
1. findNode : negated conditional → KILLED |
if (this.lookupTraversalDirection == WordTraversalDirection.BACKWARD) { |
| 1570 |
4
1. findNode : Replaced integer subtraction with addition → KILLED 2. findNode : Replaced integer addition with subtraction → KILLED 3. findNode : negated conditional → KILLED 4. findNode : changed conditional boundary → KILLED |
for (int traversalOffset = offset + length - 1; traversalOffset >= offset; traversalOffset--) { |
| 1571 |
1
1. findNode : negated conditional → KILLED |
if (current.acceptsRemainingInput()) { |
| 1572 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → NO_COVERAGE |
return current; |
| 1573 | } | |
| 1574 | current = current.findChild(key[traversalOffset]); | |
| 1575 |
1
1. findNode : negated conditional → KILLED |
if (current == null) { |
| 1576 | return null; | |
| 1577 | } | |
| 1578 | } | |
| 1579 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → KILLED |
return current; |
| 1580 | } | |
| 1581 | ||
| 1582 |
1
1. findNode : Replaced integer addition with subtraction → NO_COVERAGE |
final int endExclusive = offset + length; |
| 1583 |
2
1. findNode : negated conditional → NO_COVERAGE 2. findNode : changed conditional boundary → NO_COVERAGE |
for (int traversalOffset = offset; traversalOffset < endExclusive; traversalOffset++) { |
| 1584 |
1
1. findNode : negated conditional → NO_COVERAGE |
if (current.acceptsRemainingInput()) { |
| 1585 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → NO_COVERAGE |
return current; |
| 1586 | } | |
| 1587 | current = current.findChild(key[traversalOffset]); | |
| 1588 |
1
1. findNode : negated conditional → NO_COVERAGE |
if (current == null) { |
| 1589 | return null; | |
| 1590 | } | |
| 1591 | } | |
| 1592 |
1
1. findNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie::findNode → NO_COVERAGE |
return current; |
| 1593 | } | |
| 1594 | ||
| 1595 | /** | |
| 1596 | * Visits node-local values without allocating result containers. | |
| 1597 | * | |
| 1598 | * @param node resolved node, or {@code null} | |
| 1599 | * @param sink value sink | |
| 1600 | * @param maxResults maximum values to visit | |
| 1601 | * @return number of visited values | |
| 1602 | */ | |
| 1603 | private int visitNode(final CompiledNode<V> node, final EntrySink<? super V> sink, final int maxResults) { | |
| 1604 |
1
1. visitNode : negated conditional → KILLED |
if (node == null) { |
| 1605 | return 0; | |
| 1606 | } | |
| 1607 | ||
| 1608 | final V[] orderedValues = node.orderedValues(); | |
| 1609 | final int valueCount = Math.min(orderedValues.length, maxResults); | |
| 1610 |
1
1. visitNode : negated conditional → KILLED |
if (valueCount == 0) { |
| 1611 | return 0; | |
| 1612 | } | |
| 1613 | ||
| 1614 | final int[] orderedCounts = node.orderedCounts(); | |
| 1615 | int visited = 0; | |
| 1616 |
2
1. visitNode : changed conditional boundary → KILLED 2. visitNode : negated conditional → KILLED |
for (int rank = 0; rank < valueCount; rank++) { |
| 1617 |
1
1. visitNode : Changed increment from 1 to -1 → KILLED |
visited++; |
| 1618 |
1
1. visitNode : negated conditional → KILLED |
if (!sink.accept(orderedValues[rank], orderedCounts[rank], rank)) { |
| 1619 | break; | |
| 1620 | } | |
| 1621 | } | |
| 1622 |
1
1. visitNode : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie::visitNode → KILLED |
return visited; |
| 1623 | } | |
| 1624 | ||
| 1625 | /** | |
| 1626 | * Validates visitor maximum result count. | |
| 1627 | * | |
| 1628 | * @param maxResults maximum result count | |
| 1629 | */ | |
| 1630 | private static void validateMaxResults(final int maxResults) { | |
| 1631 |
2
1. validateMaxResults : negated conditional → KILLED 2. validateMaxResults : changed conditional boundary → KILLED |
if (maxResults < 0) { |
| 1632 | throw new IllegalArgumentException("maxResults must be non-negative."); | |
| 1633 | } | |
| 1634 | } | |
| 1635 | ||
| 1636 | /** | |
| 1637 | * Applies lookup-time case normalization according to persisted metadata. | |
| 1638 | * | |
| 1639 | * @param key lookup key | |
| 1640 | * @return normalized key for trie traversal | |
| 1641 | */ | |
| 1642 | private String normalizeLookupKey(final String key) { | |
| 1643 |
1
1. normalizeLookupKey : replaced return value with "" for org/egothor/stemmer/FrequencyTrie::normalizeLookupKey → KILLED |
return normalizeLookupKey((CharSequence) key).toString(); |
| 1644 | } | |
| 1645 | ||
| 1646 | /** | |
| 1647 | * Applies lookup-time normalization according to persisted metadata. | |
| 1648 | * | |
| 1649 | * @param key lookup key | |
| 1650 | * @return normalized key for trie traversal | |
| 1651 | */ | |
| 1652 | private CharSequence normalizeLookupKey(final CharSequence key) { | |
| 1653 |
2
1. normalizeLookupKey : negated conditional → SURVIVED 2. normalizeLookupKey : negated conditional → KILLED |
if (!this.lowercasesLookupKeys && !this.removeDiacritics) { |
| 1654 |
1
1. normalizeLookupKey : replaced return value with null for org/egothor/stemmer/FrequencyTrie::normalizeLookupKey → KILLED |
return key; |
| 1655 | } | |
| 1656 | ||
| 1657 | String normalized = key.toString(); | |
| 1658 |
1
1. normalizeLookupKey : negated conditional → KILLED |
if (this.lowercasesLookupKeys) { |
| 1659 | normalized = normalized.toLowerCase(Locale.ROOT); | |
| 1660 | } | |
| 1661 |
1
1. normalizeLookupKey : negated conditional → KILLED |
if (this.removeDiacritics) { |
| 1662 | normalized = DiacriticStripper.strip(normalized); | |
| 1663 |
1
1. normalizeLookupKey : negated conditional → KILLED |
} else if (this.metadata.diacriticProcessingMode() == DiacriticProcessingMode.AS_IS_AND_STRIPPED_FALLBACK) { |
| 1664 | throw new UnsupportedOperationException( | |
| 1665 | "Diacritic processing mode AS_IS_AND_STRIPPED_FALLBACK is not supported yet."); | |
| 1666 | } | |
| 1667 | ||
| 1668 |
1
1. normalizeLookupKey : replaced return value with null for org/egothor/stemmer/FrequencyTrie::normalizeLookupKey → KILLED |
return normalized; |
| 1669 | } | |
| 1670 | ||
| 1671 | /** | |
| 1672 | * Builder of {@link FrequencyTrie}. | |
| 1673 | * | |
| 1674 | * <p> | |
| 1675 | * The builder is intentionally mutable and optimized for repeated | |
| 1676 | * {@link #put(String, Object)} calls. The final trie is created by | |
| 1677 | * {@link #build()}, which performs bottom-up subtree reduction and converts the | |
| 1678 | * structure to a compact immutable representation optimized for read | |
| 1679 | * operations. | |
| 1680 | * </p> | |
| 1681 | * | |
| 1682 | * <p> | |
| 1683 | * A builder is mutable and not thread-safe. Callers must externally serialize | |
| 1684 | * all access. Each call to {@link #build()} creates an immutable snapshot; | |
| 1685 | * subsequent builder updates do not affect previously built tries. | |
| 1686 | * </p> | |
| 1687 | * | |
| 1688 | * @param <V> value type | |
| 1689 | */ | |
| 1690 | public static final class Builder<V> { | |
| 1691 | ||
| 1692 | /** | |
| 1693 | * Logger of this class. | |
| 1694 | */ | |
| 1695 | private static final Logger LOGGER = Logger.getLogger(Builder.class.getName()); | |
| 1696 | ||
| 1697 | /** | |
| 1698 | * Factory used to create typed arrays. | |
| 1699 | */ | |
| 1700 | private final IntFunction<V[]> arrayFactory; | |
| 1701 | ||
| 1702 | /** | |
| 1703 | * Reduction configuration. | |
| 1704 | */ | |
| 1705 | private final ReductionSettings reductionSettings; | |
| 1706 | ||
| 1707 | /** | |
| 1708 | * Logical key traversal direction used by this builder. | |
| 1709 | */ | |
| 1710 | private final WordTraversalDirection traversalDirection; | |
| 1711 | ||
| 1712 | /** | |
| 1713 | * Dictionary case processing mode associated with this builder. | |
| 1714 | */ | |
| 1715 | private final CaseProcessingMode caseProcessingMode; | |
| 1716 | ||
| 1717 | /** | |
| 1718 | * Dictionary diacritic processing mode associated with this builder. | |
| 1719 | */ | |
| 1720 | private final DiacriticProcessingMode diacriticProcessingMode; | |
| 1721 | ||
| 1722 | /** | |
| 1723 | * Dense edge lookup span threshold. | |
| 1724 | * <p> | |
| 1725 | * This value controls a speed/memory trade-off during freezing: dense child | |
| 1726 | * lookup tables are allocated only for nodes whose child labels fit in this | |
| 1727 | * span. | |
| 1728 | * </p> | |
| 1729 | */ | |
| 1730 | private final int maxExpandedIndex; | |
| 1731 | ||
| 1732 | /** | |
| 1733 | * Mutable root node. | |
| 1734 | */ | |
| 1735 | private final MutableNode<V> root; | |
| 1736 | ||
| 1737 | /** | |
| 1738 | * Source compiled-node identity for each mutable node expanded by | |
| 1739 | * {@link FrequencyTrieBuilders#copyOf}. | |
| 1740 | */ | |
| 1741 | private final Map<MutableNode<V>, Object> compiledSources; | |
| 1742 | ||
| 1743 | /** | |
| 1744 | * Unique reduction discriminator for each locally modified reconstructed | |
| 1745 | * node. | |
| 1746 | */ | |
| 1747 | private final Map<MutableNode<V>, Object> mergeDiscriminators; | |
| 1748 | ||
| 1749 | /** | |
| 1750 | * Creates a new builder with the provided settings. | |
| 1751 | * | |
| 1752 | * <p> | |
| 1753 | * This constructor preserves the historical Egothor behavior and therefore | |
| 1754 | * traverses logical keys from their end toward their beginning. | |
| 1755 | * </p> | |
| 1756 | * | |
| 1757 | * @param arrayFactory array factory | |
| 1758 | * @param reductionSettings reduction configuration | |
| 1759 | * @throws NullPointerException if any argument is {@code null} | |
| 1760 | */ | |
| 1761 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionSettings reductionSettings) { | |
| 1762 | this(arrayFactory, reductionSettings, WordTraversalDirection.BACKWARD); | |
| 1763 | } | |
| 1764 | ||
| 1765 | /** | |
| 1766 | * Creates a new builder with the provided settings and explicit traversal | |
| 1767 | * direction. | |
| 1768 | * | |
| 1769 | * @param arrayFactory array factory | |
| 1770 | * @param reductionSettings reduction configuration | |
| 1771 | * @param traversalDirection logical key traversal direction | |
| 1772 | * @throws NullPointerException if any argument is {@code null} | |
| 1773 | */ | |
| 1774 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionSettings reductionSettings, | |
| 1775 | final WordTraversalDirection traversalDirection) { | |
| 1776 | this(arrayFactory, reductionSettings, traversalDirection, CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT); | |
| 1777 | } | |
| 1778 | ||
| 1779 | /** | |
| 1780 | * Creates a new builder with the provided settings, explicit traversal | |
| 1781 | * direction, and explicit case processing mode. | |
| 1782 | * | |
| 1783 | * @param arrayFactory array factory | |
| 1784 | * @param reductionSettings reduction configuration | |
| 1785 | * @param traversalDirection logical key traversal direction | |
| 1786 | * @param caseProcessingMode dictionary case processing mode | |
| 1787 | * @throws NullPointerException if any argument is {@code null} | |
| 1788 | */ | |
| 1789 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionSettings reductionSettings, | |
| 1790 | final WordTraversalDirection traversalDirection, final CaseProcessingMode caseProcessingMode) { | |
| 1791 | this(arrayFactory, reductionSettings, traversalDirection, caseProcessingMode, | |
| 1792 | DiacriticProcessingMode.AS_IS); | |
| 1793 | } | |
| 1794 | ||
| 1795 | /** | |
| 1796 | * Creates a new builder with the provided settings, explicit traversal | |
| 1797 | * direction, explicit case processing mode, and explicit diacritic processing | |
| 1798 | * mode. | |
| 1799 | * | |
| 1800 | * @param arrayFactory array factory | |
| 1801 | * @param reductionSettings reduction configuration | |
| 1802 | * @param traversalDirection logical key traversal direction | |
| 1803 | * @param caseProcessingMode dictionary case processing mode | |
| 1804 | * @param diacriticProcessingMode dictionary diacritic processing mode | |
| 1805 | * @throws NullPointerException if any argument is {@code null} | |
| 1806 | */ | |
| 1807 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionSettings reductionSettings, | |
| 1808 | final WordTraversalDirection traversalDirection, final CaseProcessingMode caseProcessingMode, | |
| 1809 | final DiacriticProcessingMode diacriticProcessingMode) { | |
| 1810 | this(arrayFactory, reductionSettings, traversalDirection, caseProcessingMode, diacriticProcessingMode, | |
| 1811 | CompiledNode.DEFAULT_MAX_EXPANDED_INDEX); | |
| 1812 | } | |
| 1813 | ||
| 1814 | /** | |
| 1815 | * Creates a new builder with the provided settings, explicit traversal | |
| 1816 | * direction, explicit case processing mode, explicit diacritic processing mode, | |
| 1817 | * and an explicit dense child lookup threshold. | |
| 1818 | * | |
| 1819 | * @param arrayFactory array factory | |
| 1820 | * @param reductionSettings reduction configuration | |
| 1821 | * @param traversalDirection logical key traversal direction | |
| 1822 | * @param caseProcessingMode dictionary case processing mode | |
| 1823 | * @param diacriticProcessingMode dictionary diacritic processing mode | |
| 1824 | * @param maxExpandedIndex dense lookup span override; zero disables | |
| 1825 | * dense lookup. Larger values increase direct | |
| 1826 | * indexing opportunities while potentially | |
| 1827 | * increasing materialization memory in nodes | |
| 1828 | * whose edge label span is within the limit. | |
| 1829 | * @throws NullPointerException if any argument is {@code null} | |
| 1830 | * @throws IllegalArgumentException if {@code maxExpandedIndex} is negative | |
| 1831 | */ | |
| 1832 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionSettings reductionSettings, | |
| 1833 | final WordTraversalDirection traversalDirection, final CaseProcessingMode caseProcessingMode, | |
| 1834 | final DiacriticProcessingMode diacriticProcessingMode, final int maxExpandedIndex) { | |
| 1835 | this.arrayFactory = Objects.requireNonNull(arrayFactory, "arrayFactory"); | |
| 1836 | this.reductionSettings = Objects.requireNonNull(reductionSettings, "reductionSettings"); | |
| 1837 | this.traversalDirection = Objects.requireNonNull(traversalDirection, "traversalDirection"); | |
| 1838 | this.caseProcessingMode = Objects.requireNonNull(caseProcessingMode, "caseProcessingMode"); | |
| 1839 | this.diacriticProcessingMode = Objects.requireNonNull(diacriticProcessingMode, "diacriticProcessingMode"); | |
| 1840 |
2
1. <init> : changed conditional boundary → SURVIVED 2. <init> : negated conditional → KILLED |
if (maxExpandedIndex < 0) { |
| 1841 | throw new IllegalArgumentException("maxExpandedIndex must be non-negative."); | |
| 1842 | } | |
| 1843 | this.maxExpandedIndex = maxExpandedIndex; | |
| 1844 | this.root = new MutableNode<>(); | |
| 1845 | this.compiledSources = new IdentityHashMap<>(); | |
| 1846 | this.mergeDiscriminators = new IdentityHashMap<>(); | |
| 1847 | } | |
| 1848 | ||
| 1849 | /** | |
| 1850 | * Creates a new builder using default thresholds for the supplied reduction | |
| 1851 | * mode. | |
| 1852 | * | |
| 1853 | * <p> | |
| 1854 | * This constructor preserves the historical Egothor behavior and therefore | |
| 1855 | * traverses logical keys from their end toward their beginning. | |
| 1856 | * </p> | |
| 1857 | * | |
| 1858 | * @param arrayFactory array factory | |
| 1859 | * @param reductionMode reduction mode | |
| 1860 | * @throws NullPointerException if any argument is {@code null} | |
| 1861 | */ | |
| 1862 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionMode reductionMode) { | |
| 1863 | this(arrayFactory, ReductionSettings.withDefaults(reductionMode), WordTraversalDirection.BACKWARD); | |
| 1864 | } | |
| 1865 | ||
| 1866 | /** | |
| 1867 | * Creates a new builder using default thresholds for the supplied reduction | |
| 1868 | * mode and explicit traversal direction. | |
| 1869 | * | |
| 1870 | * @param arrayFactory array factory | |
| 1871 | * @param reductionMode reduction mode | |
| 1872 | * @param traversalDirection logical key traversal direction | |
| 1873 | * @throws NullPointerException if any argument is {@code null} | |
| 1874 | */ | |
| 1875 | public Builder(final IntFunction<V[]> arrayFactory, final ReductionMode reductionMode, | |
| 1876 | final WordTraversalDirection traversalDirection) { | |
| 1877 | this(arrayFactory, ReductionSettings.withDefaults(reductionMode), traversalDirection); | |
| 1878 | } | |
| 1879 | ||
| 1880 | /** | |
| 1881 | * Stores a value for the supplied key and increments its local frequency. | |
| 1882 | * | |
| 1883 | * <p> | |
| 1884 | * Values are stored at the node addressed by the full key. Since trie values | |
| 1885 | * may also appear on internal nodes, an empty key is valid and stores a value | |
| 1886 | * directly at the root. | |
| 1887 | * | |
| 1888 | * @param key key | |
| 1889 | * @param value value | |
| 1890 | * @return this builder | |
| 1891 | * @throws NullPointerException if {@code key} or {@code value} is {@code null} | |
| 1892 | */ | |
| 1893 | public Builder<V> put(final String key, final V value) { | |
| 1894 |
1
1. put : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::put → KILLED |
return put(key, value, 1); |
| 1895 | } | |
| 1896 | ||
| 1897 | /** | |
| 1898 | * Builds a compiled read-only trie. | |
| 1899 | * | |
| 1900 | * @return compiled trie | |
| 1901 | * @throws ArithmeticException if reduction would aggregate a local value | |
| 1902 | * count beyond {@link Integer#MAX_VALUE} | |
| 1903 | */ | |
| 1904 | public FrequencyTrie<V> build() { | |
| 1905 | if (LOGGER.isLoggable(Level.FINE)) { | |
| 1906 | LOGGER.log(Level.FINE, "Starting trie compilation with reduction mode {0}.", | |
| 1907 | this.reductionSettings.reductionMode()); | |
| 1908 | } | |
| 1909 | ||
| 1910 | final ReductionContext<V> reductionContext = new ReductionContext<>(this.reductionSettings); | |
| 1911 | final ReducedNode<V> reducedRoot = reduce(this.root, reductionContext); | |
| 1912 | final CompiledNode<V> compiledRoot = freeze(reducedRoot, new IdentityHashMap<>()); | |
| 1913 | ||
| 1914 | if (LOGGER.isLoggable(Level.FINE)) { | |
| 1915 | LOGGER.log(Level.FINE, "Trie compilation finished. Canonical node count: {0}.", | |
| 1916 | reductionContext.canonicalNodeCount()); | |
| 1917 | } | |
| 1918 | ||
| 1919 | final TrieMetadata metadata = TrieMetadata.forCompilation(this.traversalDirection, this.reductionSettings, | |
| 1920 | this.diacriticProcessingMode, this.caseProcessingMode); | |
| 1921 |
1
1. build : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::build → KILLED |
return new FrequencyTrie<>(this.arrayFactory, compiledRoot, metadata); |
| 1922 | } | |
| 1923 | ||
| 1924 | /** | |
| 1925 | * Stores a value for the supplied key and increments its local frequency by the | |
| 1926 | * specified positive count. | |
| 1927 | * | |
| 1928 | * <p> | |
| 1929 | * Values are stored at the node addressed by the full key. Since trie values | |
| 1930 | * may also appear on internal nodes, an empty key is valid and stores a value | |
| 1931 | * directly at the root. | |
| 1932 | * | |
| 1933 | * <p> | |
| 1934 | * This method is functionally equivalent to calling | |
| 1935 | * {@link #put(String, Object)} repeatedly {@code count} times, but it avoids | |
| 1936 | * unnecessary repeated map updates and is therefore preferable for bulk | |
| 1937 | * reconstruction from compiled tries or other aggregated sources. | |
| 1938 | * | |
| 1939 | * @param key key | |
| 1940 | * @param value value | |
| 1941 | * @param count positive frequency increment | |
| 1942 | * @return this builder | |
| 1943 | * @throws NullPointerException if {@code key} or {@code value} is | |
| 1944 | * {@code null} | |
| 1945 | * @throws IllegalArgumentException if {@code count} is less than {@code 1} | |
| 1946 | * @throws ArithmeticException if the accumulated count would exceed | |
| 1947 | * {@link Integer#MAX_VALUE} | |
| 1948 | */ | |
| 1949 | public Builder<V> put(final String key, final V value, final int count) { | |
| 1950 | Objects.requireNonNull(key, ARG_KEY); | |
| 1951 | Objects.requireNonNull(value, "value"); | |
| 1952 | ||
| 1953 |
2
1. put : changed conditional boundary → KILLED 2. put : negated conditional → KILLED |
if (count < 1) { // NOPMD |
| 1954 | throw new IllegalArgumentException("count must be at least 1."); | |
| 1955 | } | |
| 1956 | ||
| 1957 | final MutableNode<V> current = navigateToNode(normalizeDictionaryKey(key)); | |
| 1958 | final Integer previous = current.valueCounts().get(value); | |
| 1959 |
1
1. put : negated conditional → KILLED |
final int updatedCount = previous == null ? count : Math.addExact(previous, count); |
| 1960 |
1
1. put : removed call to org/egothor/stemmer/FrequencyTrie$Builder::markModified → KILLED |
markModified(current); |
| 1961 | current.valueCounts().put(value, updatedCount); | |
| 1962 |
1
1. put : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::put → KILLED |
return this; |
| 1963 | } | |
| 1964 | ||
| 1965 | /** | |
| 1966 | * Makes {@code value} the dominant (highest-frequency) local value at the node | |
| 1967 | * addressed by {@code key}, keeping any other values as lower-ranked | |
| 1968 | * alternatives. | |
| 1969 | * | |
| 1970 | * <p> | |
| 1971 | * The value's count is set to one more than the highest count among the other | |
| 1972 | * values at that node, so {@link FrequencyTrie#get(String)} returns it while | |
| 1973 | * {@link FrequencyTrie#getAll(String)} still lists the alternatives. Use this | |
| 1974 | * to override a rule for one key without discarding the prior candidates. | |
| 1975 | * | |
| 1976 | * <p> | |
| 1977 | * This differs from {@link #put(String, Object, int)}, which adds a raw | |
| 1978 | * frequency that may or may not dominate, and from {@link #set(String, Object)}, | |
| 1979 | * which discards the other values entirely. | |
| 1980 | * | |
| 1981 | * @param key key | |
| 1982 | * @param value value to make dominant | |
| 1983 | * @return this builder | |
| 1984 | * @throws NullPointerException if {@code key} or {@code value} is {@code null} | |
| 1985 | * @throws ArithmeticException if another value already has a count of | |
| 1986 | * {@link Integer#MAX_VALUE} | |
| 1987 | */ | |
| 1988 | public Builder<V> putDominant(final String key, final V value) { | |
| 1989 | Objects.requireNonNull(key, ARG_KEY); | |
| 1990 | Objects.requireNonNull(value, "value"); | |
| 1991 | ||
| 1992 | final MutableNode<V> node = navigateToNode(normalizeDictionaryKey(key)); | |
| 1993 | final Map<V, Integer> counts = node.valueCounts(); | |
| 1994 | int maxOther = 0; | |
| 1995 | for (final Map.Entry<V, Integer> entry : counts.entrySet()) { | |
| 1996 |
1
1. putDominant : negated conditional → KILLED |
if (!entry.getKey().equals(value)) { |
| 1997 | maxOther = Math.max(maxOther, entry.getValue()); | |
| 1998 | } | |
| 1999 | } | |
| 2000 |
1
1. putDominant : negated conditional → KILLED |
if (maxOther == Integer.MAX_VALUE) { |
| 2001 | throw new ArithmeticException( | |
| 2002 | "Cannot make value dominant because another value already has Integer.MAX_VALUE count."); | |
| 2003 | } | |
| 2004 |
1
1. putDominant : removed call to org/egothor/stemmer/FrequencyTrie$Builder::markModified → SURVIVED |
markModified(node); |
| 2005 |
1
1. putDominant : Replaced integer addition with subtraction → KILLED |
counts.put(value, maxOther + 1); |
| 2006 |
1
1. putDominant : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::putDominant → KILLED |
return this; |
| 2007 | } | |
| 2008 | ||
| 2009 | /** | |
| 2010 | * Replaces every local value at the node addressed by {@code key} with the | |
| 2011 | * single {@code value} (count 1), making it the sole and therefore dominant | |
| 2012 | * value there. Any prior values at that node are discarded. | |
| 2013 | * | |
| 2014 | * <p> | |
| 2015 | * This differs from {@link #put(String, Object, int)} (which accumulates) and | |
| 2016 | * {@link #putDominant(String, Object)} (which dominates but keeps the | |
| 2017 | * alternatives). | |
| 2018 | * | |
| 2019 | * @param key key | |
| 2020 | * @param value replacement value | |
| 2021 | * @return this builder | |
| 2022 | * @throws NullPointerException if {@code key} or {@code value} is {@code null} | |
| 2023 | */ | |
| 2024 | public Builder<V> set(final String key, final V value) { | |
| 2025 | Objects.requireNonNull(key, ARG_KEY); | |
| 2026 | Objects.requireNonNull(value, "value"); | |
| 2027 | ||
| 2028 | final MutableNode<V> node = navigateToNode(normalizeDictionaryKey(key)); | |
| 2029 |
1
1. set : removed call to org/egothor/stemmer/FrequencyTrie$Builder::markModified → SURVIVED |
markModified(node); |
| 2030 | final Map<V, Integer> counts = node.valueCounts(); | |
| 2031 |
1
1. set : removed call to java/util/Map::clear → KILLED |
counts.clear(); |
| 2032 | counts.put(value, 1); | |
| 2033 |
1
1. set : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::set → KILLED |
return this; |
| 2034 | } | |
| 2035 | ||
| 2036 | /** | |
| 2037 | * Stores {@code value} (count 1) at the node addressed by {@code key} only when | |
| 2038 | * that node currently has no local value; otherwise the builder is unchanged. | |
| 2039 | * Use this to fill gaps without overwriting curated entries. | |
| 2040 | * | |
| 2041 | * @param key key | |
| 2042 | * @param value value to store when absent | |
| 2043 | * @return this builder | |
| 2044 | * @throws NullPointerException if {@code key} or {@code value} is {@code null} | |
| 2045 | */ | |
| 2046 | public Builder<V> putIfAbsent(final String key, final V value) { | |
| 2047 | Objects.requireNonNull(key, ARG_KEY); | |
| 2048 | Objects.requireNonNull(value, "value"); | |
| 2049 | ||
| 2050 | final MutableNode<V> node = navigateToNode(normalizeDictionaryKey(key)); | |
| 2051 | final Map<V, Integer> counts = node.valueCounts(); | |
| 2052 |
1
1. putIfAbsent : negated conditional → KILLED |
if (counts.isEmpty()) { |
| 2053 |
1
1. putIfAbsent : removed call to org/egothor/stemmer/FrequencyTrie$Builder::markModified → SURVIVED |
markModified(node); |
| 2054 | counts.put(value, 1); | |
| 2055 | } | |
| 2056 |
1
1. putIfAbsent : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::putIfAbsent → KILLED |
return this; |
| 2057 | } | |
| 2058 | ||
| 2059 | /** | |
| 2060 | * Removes every local value at the node addressed by {@code key}. The node | |
| 2061 | * structure is retained but stores no value, so {@link FrequencyTrie#get(String)} | |
| 2062 | * returns {@code null} for that exact key. A key whose path does not exist is a | |
| 2063 | * no-op; the trie structure is not pruned. | |
| 2064 | * | |
| 2065 | * <p> | |
| 2066 | * <strong>This targets the exact node for {@code key} only.</strong> A key that | |
| 2067 | * resolves through a shorter contracted generalization does not have a value at | |
| 2068 | * its own full-length node — reduction may have collapsed its rule into a higher | |
| 2069 | * suffix node — so removing the full key does not change how it resolves. To | |
| 2070 | * suppress or re-map such a key, add a specific rule instead | |
| 2071 | * ({@link #set(String, Object)} or {@link #putDominant(String, Object)}) and read | |
| 2072 | * with {@link LookupMode#LAST}, or remove the shorter generalization key (which | |
| 2073 | * affects every key it covers). | |
| 2074 | * | |
| 2075 | * @param key key whose local values are removed | |
| 2076 | * @return this builder | |
| 2077 | * @throws NullPointerException if {@code key} is {@code null} | |
| 2078 | */ | |
| 2079 | public Builder<V> remove(final String key) { | |
| 2080 | Objects.requireNonNull(key, ARG_KEY); | |
| 2081 | ||
| 2082 | final MutableNode<V> node = findMutableNode(normalizeDictionaryKey(key)); | |
| 2083 |
2
1. remove : negated conditional → KILLED 2. remove : negated conditional → KILLED |
if (node != null && !node.valueCounts().isEmpty()) { |
| 2084 |
1
1. remove : removed call to org/egothor/stemmer/FrequencyTrie$Builder::markModified → SURVIVED |
markModified(node); |
| 2085 |
1
1. remove : removed call to java/util/Map::clear → KILLED |
node.valueCounts().clear(); |
| 2086 |
1
1. remove : removed call to org/egothor/stemmer/trie/MutableNode::clearAcceptsRemainingInput → KILLED |
node.clearAcceptsRemainingInput(); |
| 2087 | } | |
| 2088 |
1
1. remove : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::remove → KILLED |
return this; |
| 2089 | } | |
| 2090 | ||
| 2091 | /** | |
| 2092 | * Removes a single {@code value} from the node addressed by {@code key}, | |
| 2093 | * keeping any other values there. A missing key or value is a no-op. | |
| 2094 | * | |
| 2095 | * @param key key | |
| 2096 | * @param value value to remove | |
| 2097 | * @return this builder | |
| 2098 | * @throws NullPointerException if {@code key} or {@code value} is {@code null} | |
| 2099 | */ | |
| 2100 | public Builder<V> remove(final String key, final V value) { | |
| 2101 | Objects.requireNonNull(key, ARG_KEY); | |
| 2102 | Objects.requireNonNull(value, "value"); | |
| 2103 | ||
| 2104 | final MutableNode<V> node = findMutableNode(normalizeDictionaryKey(key)); | |
| 2105 |
2
1. remove : negated conditional → KILLED 2. remove : negated conditional → KILLED |
if (node != null && node.valueCounts().containsKey(value)) { |
| 2106 |
1
1. remove : removed call to org/egothor/stemmer/FrequencyTrie$Builder::markModified → SURVIVED |
markModified(node); |
| 2107 | node.valueCounts().remove(value); | |
| 2108 |
1
1. remove : negated conditional → SURVIVED |
if (node.valueCounts().isEmpty()) { |
| 2109 |
1
1. remove : removed call to org/egothor/stemmer/trie/MutableNode::clearAcceptsRemainingInput → NO_COVERAGE |
node.clearAcceptsRemainingInput(); |
| 2110 | } | |
| 2111 | } | |
| 2112 |
1
1. remove : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::remove → KILLED |
return this; |
| 2113 | } | |
| 2114 | ||
| 2115 | /** | |
| 2116 | * Navigates to the mutable node addressed by an already-normalized key without | |
| 2117 | * creating missing nodes. | |
| 2118 | * | |
| 2119 | * @param normalizedKey already-normalized key | |
| 2120 | * @return addressed node, or {@code null} if the path does not exist | |
| 2121 | */ | |
| 2122 | private MutableNode<V> findMutableNode(final String normalizedKey) { | |
| 2123 | MutableNode<V> current = this.root; | |
| 2124 |
2
1. findMutableNode : changed conditional boundary → KILLED 2. findMutableNode : negated conditional → KILLED |
for (int traversalOffset = 0; traversalOffset < normalizedKey.length(); traversalOffset++) { |
| 2125 | final Character edge = normalizedKey | |
| 2126 | .charAt(this.traversalDirection.logicalIndex(normalizedKey.length(), traversalOffset)); | |
| 2127 | current = current.children().get(edge); | |
| 2128 |
1
1. findMutableNode : negated conditional → KILLED |
if (current == null) { |
| 2129 | return null; | |
| 2130 | } | |
| 2131 | } | |
| 2132 |
1
1. findMutableNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::findMutableNode → KILLED |
return current; |
| 2133 | } | |
| 2134 | ||
| 2135 | /** | |
| 2136 | * Navigates to (creating as needed) the mutable node addressed by an | |
| 2137 | * already-normalized key, consuming characters in traversal-direction order. | |
| 2138 | * | |
| 2139 | * @param normalizedKey already-normalized dictionary key | |
| 2140 | * @return addressed mutable node | |
| 2141 | */ | |
| 2142 | private MutableNode<V> navigateToNode(final String normalizedKey) { | |
| 2143 | MutableNode<V> current = this.root; | |
| 2144 |
2
1. navigateToNode : changed conditional boundary → KILLED 2. navigateToNode : negated conditional → KILLED |
for (int traversalOffset = 0; traversalOffset < normalizedKey.length(); traversalOffset++) { |
| 2145 | final Character edge = normalizedKey | |
| 2146 | .charAt(this.traversalDirection.logicalIndex(normalizedKey.length(), traversalOffset)); | |
| 2147 | MutableNode<V> child = current.children().get(edge); | |
| 2148 |
1
1. navigateToNode : negated conditional → KILLED |
if (child == null) { |
| 2149 | child = new MutableNode<>(); // NOPMD | |
| 2150 | current.children().put(edge, child); | |
| 2151 | } | |
| 2152 | current = child; | |
| 2153 | } | |
| 2154 |
1
1. navigateToNode : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::navigateToNode → KILLED |
return current; |
| 2155 | } | |
| 2156 | ||
| 2157 | /** | |
| 2158 | * Marks the node addressed by {@code key} as accepting any remaining input, | |
| 2159 | * so reduction preserves a compiled trie's contracted generalization. The | |
| 2160 | * node can later acquire children when a more-specific override is added. | |
| 2161 | * Used by {@link FrequencyTrieBuilders#copyOf} when reconstructing a writable | |
| 2162 | * builder from a compiled trie. | |
| 2163 | * | |
| 2164 | * @param key logical key of the accepting node | |
| 2165 | * @return this builder | |
| 2166 | * @throws NullPointerException if {@code key} is {@code null} | |
| 2167 | */ | |
| 2168 | /* default */ Builder<V> markAcceptsRemainingInput(final String key) { | |
| 2169 | Objects.requireNonNull(key, ARG_KEY); | |
| 2170 |
1
1. markAcceptsRemainingInput : removed call to org/egothor/stemmer/trie/MutableNode::markAcceptsRemainingInput → KILLED |
navigateToNode(normalizeDictionaryKey(key)).markAcceptsRemainingInput(); |
| 2171 |
1
1. markAcceptsRemainingInput : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::markAcceptsRemainingInput → SURVIVED |
return this; |
| 2172 | } | |
| 2173 | ||
| 2174 | /** | |
| 2175 | * Associates the mutable node at {@code key} with its source compiled DAG | |
| 2176 | * node. | |
| 2177 | * | |
| 2178 | * <p> | |
| 2179 | * The association prevents already-aggregated counts from being multiplied | |
| 2180 | * when shared compiled nodes are expanded to logical paths and then reduced | |
| 2181 | * again. This reconstruction-only operation must be invoked after copying | |
| 2182 | * the source node's values and accepting state. | |
| 2183 | * </p> | |
| 2184 | * | |
| 2185 | * @param key logical key of the expanded node | |
| 2186 | * @param sourceIdentity non-null source compiled-node identity | |
| 2187 | * @return this builder | |
| 2188 | * @throws NullPointerException if either argument is {@code null} | |
| 2189 | * @throws IllegalStateException if the addressed mutable node was already | |
| 2190 | * associated with a different compiled node | |
| 2191 | */ | |
| 2192 | /* default */ Builder<V> recordCompiledSource(final String key, final Object sourceIdentity) { | |
| 2193 | Objects.requireNonNull(key, ARG_KEY); | |
| 2194 | Objects.requireNonNull(sourceIdentity, "sourceIdentity"); | |
| 2195 | final MutableNode<V> node = navigateToNode(normalizeDictionaryKey(key)); | |
| 2196 | final Object previous = this.compiledSources.putIfAbsent(node, sourceIdentity); | |
| 2197 |
2
1. recordCompiledSource : negated conditional → NO_COVERAGE 2. recordCompiledSource : negated conditional → KILLED |
if (previous != null && previous != sourceIdentity) { // NOPMD - graph identity is intentional |
| 2198 | throw new IllegalStateException( | |
| 2199 | "Mutable node is already associated with another compiled source node."); | |
| 2200 | } | |
| 2201 |
1
1. recordCompiledSource : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::recordCompiledSource → SURVIVED |
return this; |
| 2202 | } | |
| 2203 | ||
| 2204 | /** | |
| 2205 | * Establishes a copy-on-write reduction boundary after a reconstructed | |
| 2206 | * node's local state changes. | |
| 2207 | * | |
| 2208 | * <p> | |
| 2209 | * Nodes created by ordinary insertion need no discriminator and continue to | |
| 2210 | * use the configured semantic subtree reduction. For a reconstructed node, | |
| 2211 | * the first successful update installs one stable identity token so it can | |
| 2212 | * no longer merge back into unchanged logical paths expanded from the same | |
| 2213 | * compiled DAG node. | |
| 2214 | * </p> | |
| 2215 | * | |
| 2216 | * @param node successfully modified mutable node | |
| 2217 | */ | |
| 2218 | private void markModified(final MutableNode<V> node) { | |
| 2219 |
1
1. markModified : negated conditional → KILLED |
if (this.compiledSources.containsKey(node)) { |
| 2220 |
1
1. lambda$markModified$0 : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::lambda$markModified$0 → KILLED |
this.mergeDiscriminators.computeIfAbsent(node, ignored -> new Object()); |
| 2221 | } | |
| 2222 | } | |
| 2223 | ||
| 2224 | /** | |
| 2225 | * Applies build-time dictionary-key normalization according to the builder | |
| 2226 | * configuration. | |
| 2227 | * | |
| 2228 | * @param key dictionary key | |
| 2229 | * @return normalized key for trie insertion | |
| 2230 | */ | |
| 2231 | private String normalizeDictionaryKey(final String key) { | |
| 2232 | String normalized = key; | |
| 2233 | ||
| 2234 |
1
1. normalizeDictionaryKey : negated conditional → KILLED |
if (this.caseProcessingMode == CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT) { |
| 2235 | normalized = normalized.toLowerCase(Locale.ROOT); | |
| 2236 | } | |
| 2237 | ||
| 2238 |
1
1. normalizeDictionaryKey : negated conditional → KILLED |
if (this.diacriticProcessingMode == DiacriticProcessingMode.REMOVE) { |
| 2239 | normalized = DiacriticStripper.strip(normalized); | |
| 2240 |
1
1. normalizeDictionaryKey : negated conditional → KILLED |
} else if (this.diacriticProcessingMode == DiacriticProcessingMode.AS_IS_AND_STRIPPED_FALLBACK) { |
| 2241 | throw new UnsupportedOperationException( | |
| 2242 | "Diacritic processing mode AS_IS_AND_STRIPPED_FALLBACK is not supported yet."); | |
| 2243 | } | |
| 2244 | ||
| 2245 |
1
1. normalizeDictionaryKey : replaced return value with "" for org/egothor/stemmer/FrequencyTrie$Builder::normalizeDictionaryKey → KILLED |
return normalized; |
| 2246 | } | |
| 2247 | ||
| 2248 | /** | |
| 2249 | * Returns the number of mutable build-time nodes currently reachable from the | |
| 2250 | * builder root. | |
| 2251 | * | |
| 2252 | * <p> | |
| 2253 | * This metric is intended mainly for diagnostics and tests that compare the | |
| 2254 | * unreduced build-time structure with the final reduced compiled trie. | |
| 2255 | * | |
| 2256 | * @return number of mutable build-time nodes | |
| 2257 | */ | |
| 2258 | /* default */ int buildTimeSize() { | |
| 2259 |
1
1. buildTimeSize : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie$Builder::buildTimeSize → KILLED |
return countMutableNodes(this.root); |
| 2260 | } | |
| 2261 | ||
| 2262 | /** | |
| 2263 | * Returns the logical key traversal direction used by this builder. | |
| 2264 | * | |
| 2265 | * @return logical key traversal direction | |
| 2266 | */ | |
| 2267 | /* default */ WordTraversalDirection traversalDirection() { | |
| 2268 |
1
1. traversalDirection : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::traversalDirection → NO_COVERAGE |
return this.traversalDirection; |
| 2269 | } | |
| 2270 | ||
| 2271 | /** | |
| 2272 | * Counts mutable nodes recursively. | |
| 2273 | * | |
| 2274 | * @param node current node | |
| 2275 | * @return reachable mutable node count | |
| 2276 | */ | |
| 2277 | private int countMutableNodes(final MutableNode<V> node) { | |
| 2278 | int count = 1; | |
| 2279 | for (MutableNode<V> child : node.children().values()) { | |
| 2280 |
1
1. countMutableNodes : Replaced integer addition with subtraction → KILLED |
count += countMutableNodes(child); |
| 2281 | } | |
| 2282 |
1
1. countMutableNodes : replaced int return with 0 for org/egothor/stemmer/FrequencyTrie$Builder::countMutableNodes → KILLED |
return count; |
| 2283 | } | |
| 2284 | ||
| 2285 | /** | |
| 2286 | * Reduces a mutable node to a canonical reduced node. | |
| 2287 | * | |
| 2288 | * @param source source mutable node | |
| 2289 | * @param context reduction context | |
| 2290 | * @return canonical reduced node | |
| 2291 | */ | |
| 2292 | private ReducedNode<V> reduce(final MutableNode<V> source, final ReductionContext<V> context) { | |
| 2293 | Map<Character, ReducedNode<V>> reducedChildren = new LinkedHashMap<>(); | |
| 2294 | ||
| 2295 | for (Map.Entry<Character, MutableNode<V>> childEntry : source.children().entrySet()) { | |
| 2296 | final ReducedNode<V> reducedChild = reduce(childEntry.getValue(), context); | |
| 2297 | reducedChildren.put(childEntry.getKey(), reducedChild); | |
| 2298 | } | |
| 2299 | ||
| 2300 | Map<V, Integer> localCounts = copyCounts(source.valueCounts()); | |
| 2301 | boolean acceptsRemainingInput = false; | |
| 2302 |
1
1. reduce : negated conditional → KILLED |
if (source.acceptsRemainingInput()) { |
| 2303 | // Preserved from a compiled trie's contracted accepting leaf (see | |
| 2304 | // FrequencyTrieBuilders.copyOf). The generalization is kept verbatim; | |
| 2305 | // any child edges added afterwards are retained so LookupMode.LAST/ALL | |
| 2306 | // can follow a more specific override under the same accepting node. | |
| 2307 | acceptsRemainingInput = true; | |
| 2308 |
1
1. reduce : negated conditional → KILLED |
} else if (context.settings().contractUniformSubtrees()) { |
| 2309 | final Map<V, Integer> contractedCounts = contractUniformSubtree(localCounts, reducedChildren); | |
| 2310 |
1
1. reduce : negated conditional → KILLED |
if (!contractedCounts.isEmpty()) { |
| 2311 | localCounts = contractedCounts; | |
| 2312 | reducedChildren = Collections.emptyMap(); | |
| 2313 | acceptsRemainingInput = true; | |
| 2314 | } | |
| 2315 | } | |
| 2316 | ||
| 2317 | final LocalValueSummary<V> localSummary = LocalValueSummary.of(localCounts, this.arrayFactory); | |
| 2318 | final ReductionSignature<V> signature = ReductionSignature.create(localSummary, reducedChildren, | |
| 2319 | context.settings(), acceptsRemainingInput, this.mergeDiscriminators.get(source)); | |
| 2320 | final Object compiledSource = this.compiledSources.get(source); | |
| 2321 | ||
| 2322 | ReducedNode<V> canonical = context.lookup(signature); | |
| 2323 |
1
1. reduce : negated conditional → KILLED |
if (canonical == null) { |
| 2324 | canonical = new ReducedNode<>(signature, localCounts, reducedChildren, acceptsRemainingInput); | |
| 2325 |
1
1. reduce : removed call to org/egothor/stemmer/trie/ReductionContext::register → KILLED |
context.register(signature, canonical); |
| 2326 |
1
1. reduce : negated conditional → KILLED |
if (compiledSource != null) { |
| 2327 | context.recordCompiledSourceContribution(canonical, compiledSource); | |
| 2328 | } | |
| 2329 |
1
1. reduce : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::reduce → KILLED |
return canonical; |
| 2330 | } | |
| 2331 | ||
| 2332 |
1
1. reduce : negated conditional → KILLED |
if (compiledSource == null |
| 2333 |
1
1. reduce : negated conditional → KILLED |
|| context.recordCompiledSourceContribution(canonical, compiledSource)) { |
| 2334 |
1
1. reduce : removed call to org/egothor/stemmer/trie/ReducedNode::mergeLocalCounts → KILLED |
canonical.mergeLocalCounts(localCounts); |
| 2335 | } | |
| 2336 |
1
1. reduce : removed call to org/egothor/stemmer/trie/ReducedNode::mergeChildren → SURVIVED |
canonical.mergeChildren(reducedChildren); |
| 2337 | ||
| 2338 |
1
1. reduce : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::reduce → KILLED |
return canonical; |
| 2339 | } | |
| 2340 | ||
| 2341 | /** | |
| 2342 | * Returns aggregated local counts when the supplied internal subtree contains | |
| 2343 | * one uniform value, otherwise {@code null}. | |
| 2344 | * | |
| 2345 | * @param localCounts local counts at the current node | |
| 2346 | * @param reducedChildren already reduced children | |
| 2347 | * @return single-value aggregate for a uniform non-leaf subtree, otherwise an | |
| 2348 | * empty map | |
| 2349 | */ | |
| 2350 | private Map<V, Integer> contractUniformSubtree(final Map<V, Integer> localCounts, | |
| 2351 | final Map<Character, ReducedNode<V>> reducedChildren) { | |
| 2352 |
1
1. contractUniformSubtree : negated conditional → KILLED |
if (reducedChildren.isEmpty()) { |
| 2353 | return Collections.emptyMap(); | |
| 2354 | } | |
| 2355 | ||
| 2356 | V uniformValue = null; | |
| 2357 | boolean valueSeen = false; | |
| 2358 | ||
| 2359 |
1
1. contractUniformSubtree : negated conditional → KILLED |
if (!localCounts.isEmpty()) { |
| 2360 |
1
1. contractUniformSubtree : negated conditional → SURVIVED |
if (localCounts.size() != SINGLE_VALUE_COUNT) { |
| 2361 | return Collections.emptyMap(); | |
| 2362 | } | |
| 2363 | final Map.Entry<V, Integer> localEntry = localCounts.entrySet().iterator().next(); | |
| 2364 | uniformValue = localEntry.getKey(); | |
| 2365 | valueSeen = true; | |
| 2366 | } | |
| 2367 | ||
| 2368 | for (ReducedNode<V> child : reducedChildren.values()) { | |
| 2369 |
1
1. contractUniformSubtree : negated conditional → KILLED |
if (!isSingleValueLeaf(child)) { |
| 2370 | return Collections.emptyMap(); | |
| 2371 | } | |
| 2372 | final Map.Entry<V, Integer> childEntry = child.localCounts().entrySet().iterator().next(); | |
| 2373 |
2
1. contractUniformSubtree : negated conditional → KILLED 2. contractUniformSubtree : negated conditional → KILLED |
if (valueSeen && !Objects.equals(uniformValue, childEntry.getKey())) { |
| 2374 | return Collections.emptyMap(); | |
| 2375 | } | |
| 2376 | uniformValue = childEntry.getKey(); | |
| 2377 | valueSeen = true; | |
| 2378 | } | |
| 2379 | ||
| 2380 |
1
1. contractUniformSubtree : negated conditional → KILLED |
if (!valueSeen) { |
| 2381 | return Collections.emptyMap(); | |
| 2382 | } | |
| 2383 | ||
| 2384 | final Map<V, Integer> contractedCounts = new LinkedHashMap<>(SINGLE_VALUE_COUNT); | |
| 2385 | contractedCounts.put(uniformValue, SINGLE_VALUE_COUNT); | |
| 2386 |
1
1. contractUniformSubtree : replaced return value with Collections.emptyMap for org/egothor/stemmer/FrequencyTrie$Builder::contractUniformSubtree → KILLED |
return contractedCounts; |
| 2387 | } | |
| 2388 | ||
| 2389 | /** | |
| 2390 | * Returns whether the reduced node is a leaf with exactly one stored value. | |
| 2391 | * | |
| 2392 | * @param node node to inspect | |
| 2393 | * @return {@code true} when the node can participate in uniform contraction | |
| 2394 | */ | |
| 2395 | private boolean isSingleValueLeaf(final ReducedNode<V> node) { | |
| 2396 |
3
1. isSingleValueLeaf : negated conditional → KILLED 2. isSingleValueLeaf : replaced boolean return with true for org/egothor/stemmer/FrequencyTrie$Builder::isSingleValueLeaf → KILLED 3. isSingleValueLeaf : negated conditional → KILLED |
return node.children().isEmpty() && node.localCounts().size() == SINGLE_VALUE_COUNT; |
| 2397 | } | |
| 2398 | ||
| 2399 | /** | |
| 2400 | * Freezes a reduced node into an immutable compiled node. | |
| 2401 | * | |
| 2402 | * @param reducedNode reduced node | |
| 2403 | * @param cache already frozen nodes | |
| 2404 | * @return immutable compiled node | |
| 2405 | */ | |
| 2406 | private CompiledNode<V> freeze(final ReducedNode<V> reducedNode, | |
| 2407 | final Map<ReducedNode<V>, CompiledNode<V>> cache) { | |
| 2408 | final CompiledNode<V> existing = cache.get(reducedNode); | |
| 2409 |
1
1. freeze : negated conditional → KILLED |
if (existing != null) { |
| 2410 |
1
1. freeze : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::freeze → KILLED |
return existing; |
| 2411 | } | |
| 2412 | ||
| 2413 | final LocalValueSummary<V> localSummary = LocalValueSummary.of(reducedNode.localCounts(), | |
| 2414 | this.arrayFactory); | |
| 2415 | ||
| 2416 | final List<Map.Entry<Character, ReducedNode<V>>> childEntries = new ArrayList<>( | |
| 2417 | reducedNode.children().entrySet()); | |
| 2418 |
1
1. freeze : removed call to java/util/List::sort → KILLED |
childEntries.sort(Map.Entry.comparingByKey()); |
| 2419 | ||
| 2420 | final char[] edges = new char[childEntries.size()]; | |
| 2421 | @SuppressWarnings("unchecked") | |
| 2422 | final CompiledNode<V>[] childNodes = new CompiledNode[childEntries.size()]; | |
| 2423 | ||
| 2424 |
2
1. freeze : changed conditional boundary → KILLED 2. freeze : negated conditional → KILLED |
for (int index = 0; index < childEntries.size(); index++) { |
| 2425 | final Map.Entry<Character, ReducedNode<V>> entry = childEntries.get(index); | |
| 2426 | edges[index] = entry.getKey(); | |
| 2427 | childNodes[index] = freeze(entry.getValue(), cache); | |
| 2428 | } | |
| 2429 | ||
| 2430 | final CompiledNode<V> frozen = new CompiledNode<>(edges, childNodes, localSummary.orderedValues(), | |
| 2431 | reducedNode.acceptsRemainingInput(), this.maxExpandedIndex, localSummary.orderedCounts()); | |
| 2432 | cache.put(reducedNode, frozen); | |
| 2433 |
1
1. freeze : replaced return value with null for org/egothor/stemmer/FrequencyTrie$Builder::freeze → KILLED |
return frozen; |
| 2434 | } | |
| 2435 | ||
| 2436 | /** | |
| 2437 | * Creates a shallow frequency copy preserving deterministic insertion order of | |
| 2438 | * first occurrence. | |
| 2439 | * | |
| 2440 | * @param source source counts | |
| 2441 | * @return copied counts | |
| 2442 | */ | |
| 2443 | private Map<V, Integer> copyCounts(final Map<V, Integer> source) { | |
| 2444 |
1
1. copyCounts : replaced return value with Collections.emptyMap for org/egothor/stemmer/FrequencyTrie$Builder::copyCounts → KILLED |
return new LinkedHashMap<>(source); |
| 2445 | } | |
| 2446 | } | |
| 2447 | ||
| 2448 | /** | |
| 2449 | * Reads one final trie value using metadata parsed from the same binary stream. | |
| 2450 | * | |
| 2451 | * <p> | |
| 2452 | * Implementations are invoked only during deserialization and are not retained | |
| 2453 | * by the resulting trie. Returned values are stored directly in final compiled | |
| 2454 | * node arrays. | |
| 2455 | * </p> | |
| 2456 | * | |
| 2457 | * @param <V> final value type | |
| 2458 | */ | |
| 2459 | /* default */ | |
| 2460 | @FunctionalInterface | |
| 2461 | interface MetadataValueStreamReader<V> { | |
| 2462 | ||
| 2463 | /** | |
| 2464 | * Reads and materializes one final value. | |
| 2465 | * | |
| 2466 | * @param dataInput source data input | |
| 2467 | * @param metadata already parsed trie metadata | |
| 2468 | * @return final value to store directly in compiled nodes | |
| 2469 | * @throws IOException if reading or materialization fails | |
| 2470 | */ | |
| 2471 | V read(DataInputStream dataInput, TrieMetadata metadata) throws IOException; | |
| 2472 | } | |
| 2473 | ||
| 2474 | /** | |
| 2475 | * Codec used to persist values stored in the trie. | |
| 2476 | * | |
| 2477 | * @param <V> value type | |
| 2478 | */ | |
| 2479 | public interface ValueStreamCodec<V> { | |
| 2480 | ||
| 2481 | /** | |
| 2482 | * Writes one value to the supplied data output. | |
| 2483 | * | |
| 2484 | * @param dataOutput target data output | |
| 2485 | * @param value value to write | |
| 2486 | * @throws IOException if writing fails | |
| 2487 | */ | |
| 2488 | void write(DataOutputStream dataOutput, V value) throws IOException; | |
| 2489 | ||
| 2490 | /** | |
| 2491 | * Reads one value from the supplied data input. | |
| 2492 | * | |
| 2493 | * @param dataInput source data input | |
| 2494 | * @return read value | |
| 2495 | * @throws IOException if reading fails | |
| 2496 | */ | |
| 2497 | V read(DataInputStream dataInput) throws IOException; | |
| 2498 | } | |
| 2499 | ||
| 2500 | } | |
Mutations | ||
| 263 |
1.1 |
|
| 275 |
1.1 2.2 3.3 |
|
| 315 |
1.1 |
|
| 316 |
1.1 |
|
| 319 |
1.1 |
|
| 350 |
1.1 |
|
| 374 |
1.1 |
|
| 375 |
1.1 |
|
| 377 |
1.1 |
|
| 392 |
1.1 |
|
| 420 |
1.1 |
|
| 423 |
1.1 |
|
| 427 |
1.1 |
|
| 430 |
1.1 |
|
| 451 |
1.1 |
|
| 454 |
1.1 |
|
| 458 |
1.1 |
|
| 461 |
1.1 |
|
| 481 |
1.1 |
|
| 484 |
1.1 |
|
| 488 |
1.1 |
|
| 491 |
1.1 |
|
| 527 |
1.1 |
|
| 528 |
1.1 |
|
| 531 |
1.1 |
|
| 534 |
1.1 |
|
| 535 |
1.1 |
|
| 538 |
1.1 |
|
| 539 |
1.1 |
|
| 541 |
1.1 |
|
| 571 |
1.1 |
|
| 572 |
1.1 |
|
| 575 |
1.1 |
|
| 578 |
1.1 |
|
| 584 |
1.1 |
|
| 588 |
1.1 |
|
| 589 |
1.1 |
|
| 594 |
1.1 2.2 |
|
| 597 |
1.1 |
|
| 628 |
1.1 |
|
| 629 |
1.1 |
|
| 632 |
1.1 |
|
| 633 |
1.1 |
|
| 637 |
1.1 |
|
| 640 |
1.1 |
|
| 659 |
1.1 |
|
| 660 |
1.1 |
|
| 663 |
1.1 |
|
| 664 |
1.1 |
|
| 667 |
1.1 |
|
| 670 |
1.1 |
|
| 686 |
1.1 2.2 |
|
| 699 |
1.1 2.2 |
|
| 720 |
1.1 |
|
| 721 |
1.1 |
|
| 725 |
1.1 |
|
| 726 |
1.1 |
|
| 729 |
1.1 |
|
| 732 |
1.1 |
|
| 745 |
1.1 2.2 |
|
| 759 |
1.1 |
|
| 768 |
1.1 |
|
| 795 |
1.1 |
|
| 815 |
1.1 |
|
| 832 |
1.1 |
|
| 835 |
1.1 |
|
| 839 |
1.1 |
|
| 841 |
1.1 |
|
| 847 |
1.1 |
|
| 848 |
1.1 |
|
| 849 |
1.1 |
|
| 853 |
1.1 |
|
| 855 |
1.1 |
|
| 856 |
1.1 |
|
| 858 |
1.1 |
|
| 860 |
1.1 |
|
| 869 |
1.1 |
|
| 894 |
1.1 |
|
| 902 |
1.1 |
|
| 905 |
1.1 |
|
| 911 |
1.1 |
|
| 912 |
1.1 |
|
| 913 |
1.1 |
|
| 914 |
1.1 |
|
| 915 |
1.1 |
|
| 916 |
1.1 |
|
| 918 |
1.1 2.2 |
|
| 919 |
1.1 |
|
| 922 |
1.1 |
|
| 942 |
1.1 |
|
| 968 |
1.1 |
|
| 969 |
1.1 |
|
| 997 |
1.1 |
|
| 1009 |
1.1 |
|
| 1026 |
1.1 |
|
| 1043 |
1.1 |
|
| 1044 |
1.1 |
|
| 1057 |
1.1 |
|
| 1066 |
1.1 |
|
| 1089 |
1.1 |
|
| 1109 |
1.1 |
|
| 1111 |
1.1 |
|
| 1128 |
1.1 |
|
| 1129 |
1.1 |
|
| 1130 |
1.1 2.2 |
|
| 1131 |
1.1 |
|
| 1133 |
1.1 |
|
| 1136 |
1.1 |
|
| 1139 |
1.1 |
|
| 1140 |
1.1 2.2 |
|
| 1143 |
1.1 |
|
| 1144 |
1.1 |
|
| 1149 |
1.1 |
|
| 1150 |
1.1 |
|
| 1156 |
1.1 |
|
| 1169 |
1.1 2.2 |
|
| 1170 |
1.1 |
|
| 1172 |
1.1 |
|
| 1176 |
1.1 |
|
| 1179 |
1.1 |
|
| 1182 |
1.1 |
|
| 1184 |
1.1 |
|
| 1187 |
1.1 |
|
| 1193 |
1.1 |
|
| 1194 |
1.1 |
|
| 1198 |
1.1 2.2 |
|
| 1199 |
1.1 2.2 |
|
| 1200 |
1.1 2.2 |
|
| 1201 |
1.1 |
|
| 1205 |
1.1 |
|
| 1207 |
1.1 2.2 3.3 |
|
| 1209 |
1.1 |
|
| 1228 |
1.1 2.2 |
|
| 1234 |
1.1 |
|
| 1239 |
1.1 2.2 3.3 4.4 |
|
| 1244 |
1.1 2.2 |
|
| 1249 |
1.1 2.2 3.3 4.4 |
|
| 1254 |
1.1 2.2 |
|
| 1257 |
1.1 2.2 |
|
| 1266 |
1.1 |
|
| 1289 |
1.1 2.2 |
|
| 1294 |
1.1 2.2 |
|
| 1297 |
1.1 |
|
| 1301 |
1.1 2.2 |
|
| 1307 |
1.1 2.2 |
|
| 1308 |
1.1 |
|
| 1312 |
1.1 2.2 |
|
| 1313 |
1.1 |
|
| 1319 |
1.1 2.2 |
|
| 1321 |
1.1 |
|
| 1328 |
1.1 |
|
| 1336 |
1.1 2.2 |
|
| 1337 |
1.1 |
|
| 1339 |
1.1 |
|
| 1346 |
1.1 |
|
| 1350 |
1.1 |
|
| 1356 |
1.1 2.2 3.3 4.4 |
|
| 1359 |
1.1 |
|
| 1373 |
1.1 2.2 |
|
| 1374 |
1.1 2.2 |
|
| 1379 |
1.1 2.2 |
|
| 1386 |
1.1 2.2 |
|
| 1391 |
1.1 |
|
| 1394 |
1.1 2.2 |
|
| 1402 |
1.1 2.2 |
|
| 1409 |
1.1 2.2 |
|
| 1410 |
1.1 2.2 |
|
| 1412 |
1.1 2.2 3.3 4.4 |
|
| 1422 |
1.1 2.2 |
|
| 1433 |
1.1 2.2 |
|
| 1438 |
1.1 |
|
| 1446 |
1.1 |
|
| 1447 |
1.1 |
|
| 1450 |
1.1 |
|
| 1462 |
1.1 2.2 |
|
| 1464 |
1.1 2.2 3.3 4.4 |
|
| 1476 |
1.1 |
|
| 1483 |
1.1 2.2 |
|
| 1484 |
1.1 2.2 3.3 |
|
| 1485 |
1.1 |
|
| 1501 |
1.1 |
|
| 1502 |
1.1 2.2 3.3 |
|
| 1503 |
1.1 |
|
| 1504 |
1.1 |
|
| 1507 |
1.1 |
|
| 1511 |
1.1 |
|
| 1514 |
1.1 2.2 |
|
| 1515 |
1.1 |
|
| 1516 |
1.1 |
|
| 1519 |
1.1 |
|
| 1523 |
1.1 |
|
| 1534 |
1.1 |
|
| 1535 |
1.1 2.2 3.3 |
|
| 1536 |
1.1 |
|
| 1537 |
1.1 |
|
| 1540 |
1.1 |
|
| 1544 |
1.1 |
|
| 1547 |
1.1 2.2 |
|
| 1548 |
1.1 |
|
| 1549 |
1.1 |
|
| 1552 |
1.1 |
|
| 1556 |
1.1 |
|
| 1569 |
1.1 |
|
| 1570 |
1.1 2.2 3.3 4.4 |
|
| 1571 |
1.1 |
|
| 1572 |
1.1 |
|
| 1575 |
1.1 |
|
| 1579 |
1.1 |
|
| 1582 |
1.1 |
|
| 1583 |
1.1 2.2 |
|
| 1584 |
1.1 |
|
| 1585 |
1.1 |
|
| 1588 |
1.1 |
|
| 1592 |
1.1 |
|
| 1604 |
1.1 |
|
| 1610 |
1.1 |
|
| 1616 |
1.1 2.2 |
|
| 1617 |
1.1 |
|
| 1618 |
1.1 |
|
| 1622 |
1.1 |
|
| 1631 |
1.1 2.2 |
|
| 1643 |
1.1 |
|
| 1653 |
1.1 2.2 |
|
| 1654 |
1.1 |
|
| 1658 |
1.1 |
|
| 1661 |
1.1 |
|
| 1663 |
1.1 |
|
| 1668 |
1.1 |
|
| 1840 |
1.1 2.2 |
|
| 1894 |
1.1 |
|
| 1921 |
1.1 |
|
| 1953 |
1.1 2.2 |
|
| 1959 |
1.1 |
|
| 1960 |
1.1 |
|
| 1962 |
1.1 |
|
| 1996 |
1.1 |
|
| 2000 |
1.1 |
|
| 2004 |
1.1 |
|
| 2005 |
1.1 |
|
| 2006 |
1.1 |
|
| 2029 |
1.1 |
|
| 2031 |
1.1 |
|
| 2033 |
1.1 |
|
| 2052 |
1.1 |
|
| 2053 |
1.1 |
|
| 2056 |
1.1 |
|
| 2083 |
1.1 2.2 |
|
| 2084 |
1.1 |
|
| 2085 |
1.1 |
|
| 2086 |
1.1 |
|
| 2088 |
1.1 |
|
| 2105 |
1.1 2.2 |
|
| 2106 |
1.1 |
|
| 2108 |
1.1 |
|
| 2109 |
1.1 |
|
| 2112 |
1.1 |
|
| 2124 |
1.1 2.2 |
|
| 2128 |
1.1 |
|
| 2132 |
1.1 |
|
| 2144 |
1.1 2.2 |
|
| 2148 |
1.1 |
|
| 2154 |
1.1 |
|
| 2170 |
1.1 |
|
| 2171 |
1.1 |
|
| 2197 |
1.1 2.2 |
|
| 2201 |
1.1 |
|
| 2219 |
1.1 |
|
| 2220 |
1.1 |
|
| 2234 |
1.1 |
|
| 2238 |
1.1 |
|
| 2240 |
1.1 |
|
| 2245 |
1.1 |
|
| 2259 |
1.1 |
|
| 2268 |
1.1 |
|
| 2280 |
1.1 |
|
| 2282 |
1.1 |
|
| 2302 |
1.1 |
|
| 2308 |
1.1 |
|
| 2310 |
1.1 |
|
| 2323 |
1.1 |
|
| 2325 |
1.1 |
|
| 2326 |
1.1 |
|
| 2329 |
1.1 |
|
| 2332 |
1.1 |
|
| 2333 |
1.1 |
|
| 2334 |
1.1 |
|
| 2336 |
1.1 |
|
| 2338 |
1.1 |
|
| 2352 |
1.1 |
|
| 2359 |
1.1 |
|
| 2360 |
1.1 |
|
| 2369 |
1.1 |
|
| 2373 |
1.1 2.2 |
|
| 2380 |
1.1 |
|
| 2386 |
1.1 |
|
| 2396 |
1.1 2.2 3.3 |
|
| 2409 |
1.1 |
|
| 2410 |
1.1 |
|
| 2418 |
1.1 |
|
| 2424 |
1.1 2.2 |
|
| 2433 |
1.1 |
|
| 2444 |
1.1 |