1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. 22 */ 23 package org.opengrok.suggest.popular.impl.chronicle; 24 25 import net.openhft.chronicle.map.ChronicleMap; 26 import org.apache.lucene.util.BytesRef; 27 import org.opengrok.suggest.popular.PopularityMap; 28 29 import java.io.File; 30 import java.io.IOException; 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Map.Entry; 37 import java.util.function.Predicate; 38 39 /** 40 * Adapter for {@link ChronicleMap} to expose only the necessary operations needed for most popular completion. 41 */ 42 public class ChronicleMapAdapter implements PopularityMap { 43 44 private ChronicleMap<BytesRef, Integer> map; 45 46 private final File chronicleMapFile; 47 ChronicleMapAdapter(final String name, final double averageKeySize, final int entries, final File file)48 public ChronicleMapAdapter(final String name, final double averageKeySize, final int entries, final File file) 49 throws IOException { 50 map = ChronicleMap.of(BytesRef.class, Integer.class) 51 .name(name) 52 .averageKeySize(averageKeySize) 53 .keyReaderAndDataAccess(BytesRefSizedReader.INSTANCE, new BytesRefDataAccess()) 54 .entries(entries) 55 .createOrRecoverPersistedTo(file); 56 this.chronicleMapFile = file; 57 } 58 59 /** {@inheritDoc} */ 60 @Override get(final BytesRef key)61 public int get(final BytesRef key) { 62 return map.getOrDefault(key, 0); 63 } 64 65 /** {@inheritDoc} */ 66 @Override increment(final BytesRef key, final int value)67 public void increment(final BytesRef key, final int value) { 68 if (value < 0) { 69 throw new IllegalArgumentException("Cannot increment by negative value " + value); 70 } 71 map.merge(key, value, Integer::sum); 72 } 73 74 /** {@inheritDoc} */ 75 @Override getPopularityData(final int page, final int pageSize)76 public List<Entry<BytesRef, Integer>> getPopularityData(final int page, final int pageSize) { 77 if (page < 0) { 78 throw new IllegalArgumentException("Cannot retrieve popularity data for negative page: " + page); 79 } 80 if (pageSize < 0) { 81 throw new IllegalArgumentException("Cannot retrieve negative number of results: " + pageSize); 82 } 83 84 List<Entry<BytesRef, Integer>> list = new ArrayList<>(map.entrySet()); 85 list.sort(Entry.<BytesRef, Integer>comparingByValue().reversed()); 86 87 int startIndex = page * pageSize; 88 if (startIndex >= list.size()) { 89 return Collections.emptyList(); 90 } 91 int endIndex = startIndex + pageSize; 92 if (endIndex > list.size()) { 93 endIndex = list.size(); 94 } 95 96 return list.subList(startIndex, endIndex); 97 } 98 99 /** 100 * Removes the entries with key that meets the predicate. 101 * @param predicate predicate which tests which entries should be removed 102 */ removeIf(final Predicate<BytesRef> predicate)103 public void removeIf(final Predicate<BytesRef> predicate) { 104 map.entrySet().removeIf(e -> predicate.test(e.getKey())); 105 } 106 107 /** 108 * Resizes the underlying {@link ChronicleMap}. 109 * @param newMapSize new entries count 110 * @param newMapAvgKey new average key size 111 * @throws IOException if some error occurred 112 */ 113 @SuppressWarnings("java:S2095") resize(final int newMapSize, final double newMapAvgKey)114 public void resize(final int newMapSize, final double newMapAvgKey) throws IOException { 115 if (newMapSize < 0) { 116 throw new IllegalArgumentException("Cannot resize chronicle map to negative size"); 117 } 118 if (newMapAvgKey < 0) { 119 throw new IllegalArgumentException("Cannot resize chronicle map to map with negative key size"); 120 } 121 122 Path tempFile = Files.createTempFile("opengrok", "chronicle"); 123 124 try { 125 map.getAll(tempFile.toFile()); 126 127 String field = map.name(); 128 129 map.close(); 130 131 Files.delete(chronicleMapFile.toPath()); 132 133 ChronicleMap<BytesRef, Integer> m = ChronicleMap.of(BytesRef.class, Integer.class) 134 .name(field) 135 .averageKeySize(newMapAvgKey) 136 .entries(newMapSize) 137 .keyReaderAndDataAccess(BytesRefSizedReader.INSTANCE, new BytesRefDataAccess()) 138 .createOrRecoverPersistedTo(chronicleMapFile); 139 m.putAll(tempFile.toFile()); 140 map = m; 141 } finally { 142 Files.delete(tempFile); 143 } 144 } 145 146 /** 147 * Closes the opened {@link ChronicleMap}. 148 */ 149 @Override close()150 public void close() { 151 map.close(); 152 } 153 154 } 155