blob: f7085e115d88b857185a80e33753ec662df8e10a [file] [log] [blame]
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -07001/*
2 * Copyright 2016 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.store.primitives.resources.impl;
18
19import com.google.common.base.Preconditions;
20import com.google.common.collect.HashMultimap;
21import com.google.common.collect.HashMultiset;
Aaron Kruglikovc0c27c02016-06-07 16:05:00 -070022import com.google.common.collect.ImmutableSet;
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -070023import com.google.common.collect.Lists;
24import com.google.common.collect.Maps;
25import com.google.common.collect.Multiset;
26import com.google.common.collect.Sets;
27import io.atomix.copycat.server.Commit;
28import io.atomix.copycat.server.Snapshottable;
29import io.atomix.copycat.server.StateMachineExecutor;
30import io.atomix.copycat.server.session.ServerSession;
31import io.atomix.copycat.server.session.SessionListener;
32import io.atomix.copycat.server.storage.snapshot.SnapshotReader;
33import io.atomix.copycat.server.storage.snapshot.SnapshotWriter;
34import io.atomix.resource.ResourceStateMachine;
35import org.onlab.util.CountDownCompleter;
36import org.onlab.util.Match;
37import org.onosproject.store.service.Versioned;
38import org.slf4j.Logger;
39
40import java.util.Arrays;
41import java.util.Collection;
42import java.util.Comparator;
43import java.util.EnumSet;
44import java.util.Map;
45import java.util.Properties;
46import java.util.Set;
47import java.util.TreeMap;
48import java.util.concurrent.atomic.AtomicLong;
49import java.util.function.BiConsumer;
50import java.util.function.BinaryOperator;
51import java.util.function.Function;
52import java.util.function.Supplier;
53import java.util.stream.Collector;
54import java.util.stream.Collectors;
55
56import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Clear;
57import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.ContainsEntry;
58import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.ContainsKey;
59import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.ContainsValue;
60import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Entries;
61import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Get;
62import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.IsEmpty;
63import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.KeySet;
64import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Keys;
65import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.MultiRemove;
66import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.MultimapCommand;
67import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Put;
68import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.RemoveAll;
69import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Replace;
70import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Size;
71import static org.onosproject.store.primitives.resources.impl.AsyncConsistentMultimapCommands.Values;
72import static org.slf4j.LoggerFactory.getLogger;
73
74/**
75 * State Machine for {@link AsyncConsistentSetMultimap} resource.
76 */
77public class AsyncConsistentSetMultimapState extends ResourceStateMachine
78 implements SessionListener, Snapshottable {
79
80 private final Logger log = getLogger(getClass());
81 private final AtomicLong globalVersion = new AtomicLong(1);
82 //TODO Add listener map here
83 private final Map<String, MapEntryValue> backingMap = Maps.newHashMap();
84
85 public AsyncConsistentSetMultimapState(Properties properties) {
86 super(properties);
87 }
88
89 @Override
90 public void snapshot(SnapshotWriter writer) {
91 }
92
93 @Override
94 public void install(SnapshotReader reader) {
95 }
96
97 @Override
98 protected void configure(StateMachineExecutor executor) {
99 executor.register(Size.class, this::size);
100 executor.register(IsEmpty.class, this::isEmpty);
101 executor.register(ContainsKey.class, this::containsKey);
102 executor.register(ContainsValue.class, this::containsValue);
103 executor.register(ContainsEntry.class, this::containsEntry);
104 executor.register(Clear.class, this::clear);
105 executor.register(KeySet.class, this::keySet);
106 executor.register(Keys.class, this::keys);
107 executor.register(Values.class, this::values);
108 executor.register(Entries.class, this::entries);
109 executor.register(Get.class, this::get);
110 executor.register(RemoveAll.class, this::removeAll);
111 executor.register(MultiRemove.class, this::multiRemove);
112 executor.register(Put.class, this::put);
113 executor.register(Replace.class, this::replace);
114 }
115
116 @Override
117 public void delete() {
118 super.delete();
119 }
120
121 /**
122 * Handles a Size commit.
123 *
124 * @param commit Size commit
125 * @return number of unique key value pairs in the multimap
126 */
127 protected int size(Commit<? extends Size> commit) {
128 try {
129 return backingMap.values()
130 .stream()
131 .map(valueCollection -> valueCollection.values().size())
132 .collect(Collectors.summingInt(size -> size));
133 } finally {
134 commit.close();
135 }
136 }
137
138 /**
139 * Handles an IsEmpty commit.
140 *
141 * @param commit IsEmpty commit
142 * @return true if the multimap contains no key-value pairs, else false
143 */
144 protected boolean isEmpty(Commit<? extends IsEmpty> commit) {
145 try {
146 return backingMap.isEmpty();
147 } finally {
148 commit.close();
149 }
150 }
151
152 /**
153 * Handles a contains key commit.
154 *
155 * @param commit ContainsKey commit
156 * @return returns true if the key is in the multimap, else false
157 */
158 protected boolean containsKey(Commit<? extends ContainsKey> commit) {
159 try {
160 return backingMap.containsKey(commit.operation().key());
161 } finally {
162 commit.close();
163 }
164 }
165
166 /**
167 * Handles a ContainsValue commit.
168 *
169 * @param commit ContainsValue commit
170 * @return true if the value is in the multimap, else false
171 */
172 protected boolean containsValue(Commit<? extends ContainsValue> commit) {
173 try {
Aaron Kruglikovc0c27c02016-06-07 16:05:00 -0700174 if (backingMap.values().isEmpty()) {
175 return false;
176 }
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -0700177 Match<byte[]> match = Match.ifValue(commit.operation().value());
178 return backingMap
179 .values()
180 .stream()
181 .anyMatch(valueList ->
Aaron Kruglikovc0c27c02016-06-07 16:05:00 -0700182 valueList
183 .values()
184 .stream()
185 .anyMatch(byteValue ->
186 match.matches(byteValue)));
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -0700187 } finally {
188 commit.close();
189 }
190 }
191
192 /**
193 * Handles a ContainsEntry commit.
194 *
195 * @param commit ContainsEntry commit
196 * @return true if the key-value pair exists, else false
197 */
198 protected boolean containsEntry(Commit<? extends ContainsEntry> commit) {
199 try {
200 MapEntryValue entryValue =
201 backingMap.get(commit.operation().key());
202 if (entryValue == null) {
203 return false;
204 } else {
205 Match valueMatch = Match.ifValue(commit.operation().value());
206 return entryValue
207 .values()
208 .stream()
209 .anyMatch(byteValue -> valueMatch.matches(byteValue));
210 }
211 } finally {
212 commit.close();
213 }
214 }
215
216 /**
217 * Handles a Clear commit.
218 *
219 * @param commit Clear commit
220 */
221 protected void clear(Commit<? extends Clear> commit) {
222 try {
223 backingMap.clear();
224 } finally {
225 commit.close();
226 }
227 }
228
229 /**
230 * Handles a KeySet commit.
231 *
232 * @param commit KeySet commit
233 * @return a set of all keys in the multimap
234 */
235 protected Set<String> keySet(Commit<? extends KeySet> commit) {
236 try {
Aaron Kruglikovc0c27c02016-06-07 16:05:00 -0700237 return ImmutableSet.copyOf(backingMap.keySet());
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -0700238 } finally {
239 commit.close();
240 }
241 }
242
243 /**
244 * Handles a Keys commit.
245 *
246 * @param commit Keys commit
247 * @return a multiset of keys with each key included an equal number of
248 * times to the total key-value pairs in which that key participates
249 */
250 protected Multiset<String> keys(Commit<? extends Keys> commit) {
251 try {
252 Multiset keys = HashMultiset.create();
253 backingMap.forEach((key, mapEntryValue) -> {
254 keys.add(key, mapEntryValue.values().size());
255 });
256 return keys;
257 } finally {
258 commit.close();
259 }
260 }
261
262 /**
263 * Handles a Values commit.
264 *
265 * @param commit Values commit
266 * @return the set of values in the multimap with duplicates included
267 */
268 protected Multiset<byte[]> values(Commit<? extends Values> commit) {
269 try {
270 return backingMap
271 .values()
272 .stream()
273 .collect(new HashMultisetValueCollector());
274 } finally {
275 commit.close();
276 }
277 }
278
279 /**
280 * Handles an Entries commit.
281 *
282 * @param commit Entries commit
283 * @return a set of all key-value pairs in the multimap
284 */
285 protected Collection<Map.Entry<String, byte[]>> entries(
286 Commit<? extends Entries> commit) {
287 try {
288 return backingMap
289 .entrySet()
290 .stream()
291 .collect(new EntrySetCollector());
292 } finally {
293 commit.close();
294 }
295 }
296
297 /**
298 * Handles a Get commit.
299 *
300 * @param commit Get commit
301 * @return the collection of values associated with the key or an empty
302 * list if none exist
303 */
304 protected Versioned<Collection<? extends byte[]>> get(
305 Commit<? extends Get> commit) {
306 try {
307 MapEntryValue mapEntryValue = backingMap.get(commit.operation().key());
308 return toVersioned(backingMap.get(commit.operation().key()));
309 } finally {
310 commit.close();
311 }
312 }
313
314 /**
315 * Handles a removeAll commit, and returns the previous mapping.
316 *
317 * @param commit removeAll commit
318 * @return collection of removed values
319 */
320 protected Versioned<Collection<? extends byte[]>> removeAll(
321 Commit<? extends RemoveAll> commit) {
322 if (!backingMap.containsKey(commit.operation().key())) {
323 commit.close();
324 return new Versioned<>(Sets.newHashSet(), -1);
325 } else {
326 return backingMap.get(commit.operation().key()).addCommit(commit);
327 }
328 }
329
330 /**
331 * Handles a multiRemove commit, returns true if the remove results in any
332 * change.
333 * @param commit multiRemove commit
334 * @return true if any change results, else false
335 */
336 protected boolean multiRemove(Commit<? extends MultiRemove> commit) {
337 if (!backingMap.containsKey(commit.operation().key())) {
338 commit.close();
339 return false;
340 } else {
341 return (backingMap
342 .get(commit.operation().key())
343 .addCommit(commit)) != null;
344 }
345 }
346
347 /**
348 * Handles a put commit, returns true if any change results from this
349 * commit.
350 * @param commit a put commit
351 * @return true if this commit results in a change, else false
352 */
353 protected boolean put(Commit<? extends Put> commit) {
354 if (commit.operation().values().isEmpty()) {
355 return false;
356 }
357 if (!backingMap.containsKey(commit.operation().key())) {
358 backingMap.put(commit.operation().key(),
359 new NonTransactionalCommit(1));
360 }
361 return backingMap
362 .get(commit.operation().key())
363 .addCommit(commit) != null;
364 }
365
366 protected Versioned<Collection<? extends byte[]>> replace(
367 Commit<? extends Replace> commit) {
368 if (!backingMap.containsKey(commit.operation().key())) {
369 backingMap.put(commit.operation().key(),
370 new NonTransactionalCommit(1));
371 }
372 return backingMap.get(commit.operation().key()).addCommit(commit);
373 }
374
375 @Override
376 public void register(ServerSession session) {
377 super.register(session);
378 }
379
380 @Override
381 public void unregister(ServerSession session) {
382 super.unregister(session);
383 }
384
385 @Override
386 public void expire(ServerSession session) {
387 super.expire(session);
388 }
389
390 @Override
391 public void close(ServerSession session) {
392 super.close(session);
393 }
394
395 private interface MapEntryValue {
396
397 /**
398 * Returns the list of raw {@code byte[]'s}.
399 *
400 * @return list of raw values
401 */
402 Collection<? extends byte[]> values();
403
404 /**
405 * Returns the version of the value.
406 *
407 * @return version
408 */
409 long version();
410
411 /**
412 * Discards the value by invoke appropriate clean up actions.
413 */
414 void discard();
415
416 /**
417 * Add a new commit and modifies the set of values accordingly.
418 * In the case of a replace or removeAll it returns the set of removed
419 * values. In the case of put or multiRemove it returns null for no
420 * change and a set of the added or removed values respectively if a
421 * change resulted.
422 *
423 * @param commit the commit to be added
424 */
425 Versioned<Collection<? extends byte[]>> addCommit(
426 Commit<? extends MultimapCommand> commit);
427 }
428
429 private class NonTransactionalCommit implements MapEntryValue {
430 private long version;
431 private final TreeMap<byte[], CountDownCompleter<Commit>>
432 valueCountdownMap = Maps.newTreeMap(new ByteArrayComparator());
433 /*This is a mapping of commits that added values to the commits
434 * removing those values, they will not be circular because keys will
435 * be exclusively Put and Replace commits and values will be exclusively
436 * Multiremove commits, each time a Put or replace is removed it should
437 * as part of closing go through and countdown each of the remove
438 * commits depending on it.*/
439 private final HashMultimap<Commit, CountDownCompleter<Commit>>
440 additiveToRemovalCommits = HashMultimap.create();
441
442 public NonTransactionalCommit(
443 long version) {
444 //Set the version to current it will only be updated once this is
445 // populated
446 this.version = globalVersion.get();
447 }
448
449 @Override
450 public Collection<? extends byte[]> values() {
Aaron Kruglikovc0c27c02016-06-07 16:05:00 -0700451 return ImmutableSet.copyOf(valueCountdownMap.keySet());
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -0700452 }
453
454 @Override
455 public long version() {
456 return version;
457 }
458
459 @Override
460 public void discard() {
461 valueCountdownMap.values().forEach(completer ->
462 completer.object().close());
463 }
464
465 @Override
466 public Versioned<Collection<? extends byte[]>> addCommit(
467 Commit<? extends MultimapCommand> commit) {
468 Preconditions.checkNotNull(commit);
469 Preconditions.checkNotNull(commit.operation());
470 Versioned<Collection<? extends byte[]>> retVersion;
471
472 if (commit.operation() instanceof Put) {
473 //Using a treeset here sanitizes the input, removing duplicates
474 Set<byte[]> valuesToAdd =
475 Sets.newTreeSet(new ByteArrayComparator());
476 ((Put) commit.operation()).values().forEach(value -> {
477 if (!valueCountdownMap.containsKey(value)) {
478 valuesToAdd.add(value);
479 }
480 });
481 if (valuesToAdd.isEmpty()) {
482 //Do not increment or add the commit if no change resulted
Aaron Kruglikov44a1fef2016-04-27 11:29:23 -0700483 commit.close();
484 return null;
485 }
486 //When all values from a commit have been removed decrement all
487 //removal commits relying on it and remove itself from the
488 //mapping of additive commits to the commits removing the
489 //values it added. (Only multiremoves will be dependent)
490 CountDownCompleter<Commit> completer =
491 new CountDownCompleter<>(commit, valuesToAdd.size(),
492 c -> {
493 if (additiveToRemovalCommits.containsKey(c)) {
494 additiveToRemovalCommits.
495 get(c).
496 forEach(countdown ->
497 countdown.countDown());
498 additiveToRemovalCommits.removeAll(c);
499 }
500 c.close();
501 });
502 retVersion = new Versioned<>(valuesToAdd, version);
503 valuesToAdd.forEach(value -> valueCountdownMap.put(value,
504 completer));
505 version++;
506 return retVersion;
507
508 } else if (commit.operation() instanceof Replace) {
509 //Will this work?? Need to check before check-in!
510 Set<byte[]> removedValues = Sets.newHashSet();
511 removedValues.addAll(valueCountdownMap.keySet());
512 retVersion = new Versioned<>(removedValues, version);
513 valueCountdownMap.values().forEach(countdown ->
514 countdown.countDown());
515 valueCountdownMap.clear();
516 Set<byte[]> valuesToAdd =
517 Sets.newTreeSet(new ByteArrayComparator());
518 ((Replace) commit.operation()).values().forEach(value -> {
519 valuesToAdd.add(value);
520 });
521 if (valuesToAdd.isEmpty()) {
522 version = globalVersion.incrementAndGet();
523 backingMap.remove(((Replace) commit.operation()).key());
524 //Order is important here, the commit must be closed last
525 //(or minimally after all uses)
526 commit.close();
527 return retVersion;
528 }
529 CountDownCompleter<Commit> completer =
530 new CountDownCompleter<>(commit, valuesToAdd.size(),
531 c -> {
532 if (additiveToRemovalCommits
533 .containsKey(c)) {
534 additiveToRemovalCommits.
535 get(c).
536 forEach(countdown ->
537 countdown.countDown());
538 additiveToRemovalCommits.
539 removeAll(c);
540 }
541 c.close();
542 });
543 valuesToAdd.forEach(value ->
544 valueCountdownMap.put(value, completer));
545 version = globalVersion.incrementAndGet();
546 return retVersion;
547
548 } else if (commit.operation() instanceof RemoveAll) {
549 Set<byte[]> removed = Sets.newHashSet();
550 //We can assume here that values only appear once and so we
551 //do not need to sanitize the return for duplicates.
552 removed.addAll(valueCountdownMap.keySet());
553 retVersion = new Versioned<>(removed, version);
554 valueCountdownMap.values().forEach(countdown ->
555 countdown.countDown());
556 valueCountdownMap.clear();
557 //In the case of a removeAll all commits will be removed and
558 //unlike the multiRemove case we do not need to consider
559 //dependencies among additive and removal commits.
560
561 //Save the key for use after the commit is closed
562 String key = ((RemoveAll) commit.operation()).key();
563 commit.close();
564 version = globalVersion.incrementAndGet();
565 backingMap.remove(key);
566 return retVersion;
567
568 } else if (commit.operation() instanceof MultiRemove) {
569 //Must first calculate how many commits the removal depends on.
570 //At this time we also sanitize the removal set by adding to a
571 //set with proper handling of byte[] equality.
572 Set<byte[]> removed = Sets.newHashSet();
573 Set<Commit> commitsRemovedFrom = Sets.newHashSet();
574 ((MultiRemove) commit.operation()).values().forEach(value -> {
575 if (valueCountdownMap.containsKey(value)) {
576 removed.add(value);
577 commitsRemovedFrom
578 .add(valueCountdownMap.get(value).object());
579 }
580 });
581 //If there is nothing to be removed no action should be taken.
582 if (removed.isEmpty()) {
583 //Do not increment or add the commit if no change resulted
584 commit.close();
585 return null;
586 }
587 //When all additive commits this depends on are closed this can
588 //be closed as well.
589 CountDownCompleter<Commit> completer =
590 new CountDownCompleter<>(commit,
591 commitsRemovedFrom.size(),
592 c -> c.close());
593 commitsRemovedFrom.forEach(commitRemovedFrom -> {
594 additiveToRemovalCommits.put(commitRemovedFrom, completer);
595 });
596 //Save key in case countdown results in closing the commit.
597 String removedKey = ((MultiRemove) commit.operation()).key();
598 removed.forEach(removedValue -> {
599 valueCountdownMap.remove(removedValue).countDown();
600 });
601 //The version is updated locally as well as globally even if
602 //this object will be removed from the map in case any other
603 //party still holds a reference to this object.
604 retVersion = new Versioned<>(removed, version);
605 version = globalVersion.incrementAndGet();
606 if (valueCountdownMap.isEmpty()) {
607 backingMap
608 .remove(removedKey);
609 }
610 return retVersion;
611
612 } else {
613 throw new IllegalArgumentException();
614 }
615 }
616 }
617
618 /**
619 * A collector that creates MapEntryValues and creates a multiset of all
620 * values in the map an equal number of times to the number of sets in
621 * which they participate.
622 */
623 private class HashMultisetValueCollector implements
624 Collector<MapEntryValue,
625 HashMultiset<byte[]>,
626 HashMultiset<byte[]>> {
627 private HashMultiset<byte[]> multiset = null;
628
629 @Override
630 public Supplier<HashMultiset<byte[]>> supplier() {
631 return new Supplier<HashMultiset<byte[]>>() {
632 @Override
633 public HashMultiset<byte[]> get() {
634 if (multiset == null) {
635 multiset = HashMultiset.create();
636 }
637 return multiset;
638 }
639 };
640 }
641
642 @Override
643 public BiConsumer<HashMultiset<byte[]>, MapEntryValue> accumulator() {
644 return (multiset, mapEntryValue) ->
645 multiset.addAll(mapEntryValue.values());
646 }
647
648 @Override
649 public BinaryOperator<HashMultiset<byte[]>> combiner() {
650 return (setOne, setTwo) -> {
651 setOne.addAll(setTwo);
652 return setOne;
653 };
654 }
655
656 @Override
657 public Function<HashMultiset<byte[]>,
658 HashMultiset<byte[]>> finisher() {
659 return (unused) -> multiset;
660 }
661
662 @Override
663 public Set<Characteristics> characteristics() {
664 return EnumSet.of(Characteristics.UNORDERED);
665 }
666 }
667
668 /**
669 * A collector that creates Entries of {@code <String, MapEntryValue>} and
670 * creates a set of entries all key value pairs in the map.
671 */
672 private class EntrySetCollector implements
673 Collector<Map.Entry<String, MapEntryValue>,
674 Set<Map.Entry<String, byte[]>>,
675 Set<Map.Entry<String, byte[]>>> {
676 private Set<Map.Entry<String, byte[]>> set = null;
677
678 @Override
679 public Supplier<Set<Map.Entry<String, byte[]>>> supplier() {
680 return new Supplier<Set<Map.Entry<String, byte[]>>>() {
681 @Override
682 public Set<Map.Entry<String, byte[]>> get() {
683 if (set == null) {
684 set = Sets.newHashSet();
685 }
686 return set;
687 }
688 };
689 }
690
691 @Override
692 public BiConsumer<Set<Map.Entry<String, byte[]>>,
693 Map.Entry<String, MapEntryValue>> accumulator() {
694 return (set, entry) -> {
695 entry
696 .getValue()
697 .values()
698 .forEach(byteValue ->
699 set.add(Maps.immutableEntry(entry.getKey(),
700 byteValue)));
701 };
702 }
703
704 @Override
705 public BinaryOperator<Set<Map.Entry<String, byte[]>>> combiner() {
706 return (setOne, setTwo) -> {
707 setOne.addAll(setTwo);
708 return setOne;
709 };
710 }
711
712 @Override
713 public Function<Set<Map.Entry<String, byte[]>>,
714 Set<Map.Entry<String, byte[]>>> finisher() {
715 return (unused) -> set;
716 }
717
718 @Override
719 public Set<Characteristics> characteristics() {
720 return EnumSet.of(Characteristics.UNORDERED);
721 }
722 }
723 /**
724 * Utility for turning a {@code MapEntryValue} to {@code Versioned}.
725 * @param value map entry value
726 * @return versioned instance or an empty list versioned -1 if argument is
727 * null
728 */
729 private Versioned<Collection<? extends byte[]>> toVersioned(
730 MapEntryValue value) {
731 return value == null ? new Versioned<>(Lists.newArrayList(), -1) :
732 new Versioned<>(value.values(),
733 value.version());
734 }
735
736 private class ByteArrayComparator implements Comparator<byte[]> {
737
738 @Override
739 public int compare(byte[] o1, byte[] o2) {
740 if (Arrays.equals(o1, o2)) {
741 return 0;
742 } else {
743 for (int i = 0; i < o1.length && i < o2.length; i++) {
744 if (o1[i] < o2[i]) {
745 return -1;
746 } else if (o1[i] > o2[i]) {
747 return 1;
748 }
749 }
750 return o1.length > o2.length ? 1 : -1;
751 }
752 }
753 }
Aaron Kruglikovc0c27c02016-06-07 16:05:00 -0700754 }