ProtoListBuilder.java
/*
* Copyright 2022 Proto Utils Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.proto;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import net.morimekta.collect.UnmodifiableList;
import net.morimekta.proto.utils.ValueUtil;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.RandomAccess;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
/**
* A list wrapping a proro message repeated field for a builder.
*
* @param <T> The item type.
*/
public class ProtoListBuilder<T>
extends AbstractList<T>
implements RandomAccess {
private transient final Message.Builder builder;
private transient final Descriptors.FieldDescriptor field;
/**
* @param builder The message builder.
* @param field The repeated field.
*/
public ProtoListBuilder(Message.Builder builder, Descriptors.FieldDescriptor field) {
requireNonNull(builder, "builder == null");
requireNonNull(field, "field == null");
if (!field.isRepeated() || field.isMapField()) {
throw new IllegalArgumentException("Not a list type: " + field);
}
this.builder = builder;
this.field = field;
}
@Override
public int size() {
return builder.getRepeatedFieldCount(field);
}
@Override
public void clear() {
builder.clearField(field);
}
@Override
@SuppressWarnings("unchecked")
public T get(int i) {
return (T) ValueUtil.toJavaValue(field, builder.getRepeatedField(field, i));
}
@Override
public T set(int index, T element) {
if (index < 0) {
throw new IllegalArgumentException("index < 0");
}
if (index > size()) {
throw new IllegalArgumentException("index > size(" + size() + ")");
}
Object protoValue = ValueUtil.toProtoValue(field, element);
if (index == size()) {
builder.addRepeatedField(field, protoValue);
return null;
}
T old = get(index);
builder.setRepeatedField(field, index, protoValue);
return old;
}
@Override
public void add(int index, T element) {
if (index < 0) {
throw new IllegalArgumentException("index < 0");
}
if (index > size()) {
throw new IllegalArgumentException("index > size(" + size() + ")");
}
Object protoValue = ValueUtil.toProtoValue(field, element);
if (index == size()) {
builder.addRepeatedField(field, protoValue);
} else {
@SuppressWarnings("unchecked")
List<Object> tmp = new ArrayList<>((List<Object>) builder.getField(field));
tmp.add(index, protoValue);
builder.setField(field, tmp);
}
}
@Override
@SuppressWarnings("unchecked")
public boolean addAll(int index, Collection<? extends T> c) {
if (index < 0) {
throw new IllegalArgumentException("index < 0");
}
if (index > size()) {
throw new IllegalArgumentException("index > size(" + size() + ")");
}
if (c.isEmpty()) {
return false;
}
if (index == size()) {
for (Object t : c) {
builder.addRepeatedField(field, ValueUtil.toProtoValue(field, t));
}
} else {
List<T> tmp = new ArrayList<>(this);
List<T> mapped = c.stream().map(i -> (T) ValueUtil.toProtoValue(field, i)).collect(Collectors.toList());
tmp.addAll(index, mapped);
builder.setField(field, tmp);
}
return true;
}
@Override
@SuppressWarnings("unchecked")
public T remove(int index) {
if (index < 0) {
throw new IllegalArgumentException("index < 0");
}
if (index >= size()) {
throw new IllegalArgumentException("index >= size(" + size() + ")");
}
if (size() == 1) {
var tmp = get(0);
clear();
return tmp;
}
@SuppressWarnings("unchecked")
List<Object> tmp = new ArrayList<>((List<Object>) builder.getField(field));
Object old = tmp.remove(index);
builder.setField(field, tmp);
return (T) ValueUtil.toJavaValue(field, old);
}
@Override
public void sort(Comparator<? super T> c) {
List<T> newList = UnmodifiableList.asList(this).sortedBy(c);
builder.setField(field, ValueUtil.toProtoValue(field, newList));
}
@Override
public Iterator<T> iterator() {
return new FieldIterator(0);
}
@Override
public ListIterator<T> listIterator() {
return new FieldIterator(0);
}
@Override
public ListIterator<T> listIterator(int index) {
if (index < 0) {
throw new IllegalArgumentException("index < 0");
}
if (index > size()) {
throw new IllegalArgumentException("index > size(" + size() + ")");
}
return new FieldIterator(index);
}
@Override
public String toString() {
return ValueUtil.asString(this);
}
private class FieldIterator
implements ListIterator<T> {
// "this" item is removed, so cannot be accessed. Next will use same index.
private int currentIndex;
private int nextIndex;
FieldIterator(int nextIndex) {
this.currentIndex = -1;
this.nextIndex = nextIndex;
}
@Override
public boolean hasNext() {
return nextIndex < size();
}
@Override
public T next() {
if (nextIndex >= size()) {
throw new NoSuchElementException(nextIndex + " >= " + size());
}
currentIndex = nextIndex;
++nextIndex;
return get(currentIndex);
}
@Override
public boolean hasPrevious() {
return nextIndex > 0;
}
@Override
public T previous() {
if (nextIndex <= 0) {
throw new NoSuchElementException(nextIndex + " <= " + 0);
}
--nextIndex;
currentIndex = nextIndex;
return get(currentIndex);
}
@Override
public int nextIndex() {
return nextIndex;
}
@Override
public int previousIndex() {
return nextIndex - 1;
}
@Override
public void remove() {
if (currentIndex < 0) {
throw new NoSuchElementException("No current item");
}
ProtoListBuilder.this.remove(currentIndex);
currentIndex = -1;
--nextIndex;
}
@Override
public void set(T t) {
if (currentIndex < 0) {
throw new NoSuchElementException("No current item");
}
ProtoListBuilder.this.set(currentIndex, t);
}
@Override
public void add(T t) {
if (currentIndex < 0) {
throw new NoSuchElementException("No current item");
}
ProtoListBuilder.this.add(currentIndex, t);
++currentIndex;
++nextIndex;
}
}
}