com.almworks.jira.structure.api.item.ItemIdentitySet Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of structure-api Show documentation
Show all versions of structure-api Show documentation
Public API for the Structure Plugin for JIRA
The newest version!
package com.almworks.jira.structure.api.item;
import com.almworks.integers.*;
import com.almworks.jira.structure.api.row.RowRetriever;
import com.atlassian.annotations.Internal;
import com.google.common.collect.AbstractIterator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.almworks.jira.structure.api.item.ItemIdentity.longId;
import static com.almworks.jira.structure.api.item.ItemIdentity.stringId;
import static com.almworks.jira.structure.api.row.ItemAccessMode.ITEM_NOT_NEEDED;
import static com.almworks.jira.structure.api.row.RowRetriever.IGNORE_MISSING_ROWS;
/**
* An implementation of a {@link Set} of {@link ItemIdentity}, optimized for memory consumption and objects count.
*
* Implementation Notes
*
* Statistics: compared to HashSet, 4x less memory taken, 1.5x slower. The number of objects is ~ 10 (compared to N*2 with
* HashSet), if all item IDs are long. String item IDs create K*2 objects as well.
*/
@Internal
public class ItemIdentitySet extends AbstractSet {
private static final int SUBSET_INITIAL_CAPACITY = 10;
@Nullable
private Map myLongIds;
@Nullable
private Map myStringIds;
private int myModCount;
private volatile boolean myImmutable;
public ItemIdentitySet() {
}
public ItemIdentitySet(ItemIdentity... ids) {
if (ids != null) {
addAll(Arrays.asList(ids));
}
}
public ItemIdentitySet(Collection extends ItemIdentity> ids) {
if (ids != null) {
addAll(ids);
}
}
public static ItemIdentitySet of(String itemType, long[] ids) {
return of(itemType, new LongArray(ids));
}
public static ItemIdentitySet of(String itemType, LongIterable ids) {
ItemIdentitySet set = new ItemIdentitySet();
set.addAll(itemType, ids);
return set;
}
public static ItemIdentitySet of(String itemType, Iterable ids) {
ItemIdentitySet set = new ItemIdentitySet();
set.addAll(itemType, ids);
return set;
}
@NotNull
@Override
public Iterator iterator() {
return new RecollectingIterator();
}
@Override
public int size() {
int r = 0;
if (myLongIds != null) {
for (COWLongSet set : myLongIds.values()) {
r += set.set().size();
}
}
if (myStringIds != null) {
for (COWStringSet set : myStringIds.values()) {
r += set.set().size();
}
}
return r;
}
@Override
public boolean contains(Object o) {
if (!(o instanceof ItemIdentity)) return false;
ItemIdentity id = (ItemIdentity) o;
if (id.isLongId()) {
if (myLongIds == null) return false;
COWLongSet set = myLongIds.get(id.getItemType());
return set != null && set.set().contains(id.getLongId());
} else {
if (myStringIds == null) return false;
COWStringSet set = myStringIds.get(id.getItemType());
return set != null && set.set().contains(id.getStringId());
}
}
@Override
public boolean containsAll(@NotNull Collection> c) {
if (c.size() > size()) return false;
if (!(c instanceof ItemIdentitySet)) {
return super.containsAll(c);
}
ItemIdentitySet set = (ItemIdentitySet) c;
if (set.myLongIds != null) {
for (Map.Entry e : set.myLongIds.entrySet()) {
if (!e.getValue().set().isEmpty()) {
if (myLongIds == null) return false;
COWLongSet longSet = myLongIds.get(e.getKey());
if (longSet == null || !longSet.set().containsAll(e.getValue().set())) return false;
}
}
}
if (set.myStringIds != null) {
for (Map.Entry e : set.myStringIds.entrySet()) {
if (!e.getValue().set().isEmpty()) {
if (myStringIds == null) return false;
COWStringSet stringSet = myStringIds.get(e.getKey());
if (stringSet == null || !stringSet.set().containsAll(e.getValue().set())) return false;
}
}
}
return true;
}
@Override
public boolean add(ItemIdentity id) {
checkImmutable();
if (id == null) {
throw new IllegalArgumentException("null id");
}
boolean changed;
String itemType = id.getItemType();
if (id.isLongId()) {
Map map = ensureLongIdsMap(1);
COWLongSet set = map.get(itemType);
if (set == null) {
set = new COWLongSet(id.getLongId());
map.put(itemType, set);
changed = true;
} else {
changed = set.add(id.getLongId());
}
} else {
Map map = ensureStringIdsMap(1);
COWStringSet set = map.get(itemType);
if (set == null) {
set = new COWStringSet(id.getStringId());
map.put(itemType, set);
changed = true;
} else {
changed = set.add(id.getStringId());
}
}
if (changed) {
myModCount++;
}
return changed;
}
@Override
public boolean addAll(@NotNull Collection extends ItemIdentity> c) {
checkImmutable();
if (c == this) {
return false;
}
if (!(c instanceof ItemIdentitySet)) {
return super.addAll(c);
}
boolean modified = false;
ItemIdentitySet set = (ItemIdentitySet) c;
if (set.myLongIds != null) {
Map map = ensureLongIdsMap(set.myLongIds.size());
for (Map.Entry e : set.myLongIds.entrySet()) {
COWLongSet longSet = map.get(e.getKey());
if (longSet == null) {
map.put(e.getKey(), e.getValue().copy(this));
modified = true;
} else {
modified |= longSet.addAll(e.getValue());
}
}
}
if (set.myStringIds != null) {
Map map = ensureStringIdsMap(set.myStringIds.size());
for (Map.Entry e : set.myStringIds.entrySet()) {
COWStringSet stringSet = map.get(e.getKey());
if (stringSet == null) {
map.put(e.getKey(), e.getValue().copy(this));
modified = true;
} else {
modified |= stringSet.addAll(e.getValue());
}
}
}
if (modified) {
myModCount++;
}
return modified;
}
public void addAll(@NotNull String type, @NotNull Iterable strings) {
for (String id: strings) {
add(stringId(type, id));
}
}
public void addAll(@NotNull String type, @NotNull LongIterable longs) {
for (LongIterator it: longs) {
add(longId(type, it.value()));
}
}
@Override
public boolean remove(Object o) {
checkImmutable();
if (!(o instanceof ItemIdentity)) return false;
ItemIdentity id = (ItemIdentity) o;
String itemType = id.getItemType();
boolean removed;
if (id.isLongId()) {
if (myLongIds == null) {
removed = false;
} else {
COWLongSet set = myLongIds.get(itemType);
removed = set != null && set.remove(id.getLongId());
if (removed && set.set().isEmpty()) {
myLongIds.remove(itemType);
}
}
} else {
if (myStringIds == null) {
removed = false;
} else {
COWStringSet set = myStringIds.get(itemType);
removed = set != null && set.remove(id.getStringId());
if (removed && set.set().isEmpty()) {
myStringIds.remove(itemType);
}
}
}
if (removed) {
myModCount++;
}
return removed;
}
@Override
public boolean removeAll(Collection> c) {
// todo could be optimized if we use it
boolean changed = false;
for (Object o : c) {
changed |= remove(o);
}
return changed;
}
@Override
public boolean retainAll(Collection> c) {
// todo optimize if we use it
ItemIdentitySet copy = copy();
boolean changed = false;
for (ItemIdentity id : copy) {
if (!c.contains(id)) {
remove(id);
changed = true;
}
}
return changed;
}
public ItemIdentitySet makeImmutable() {
myImmutable = true;
return this;
}
public boolean isImmutable() {
return myImmutable;
}
private void checkImmutable() {
if (myImmutable) {
throw new IllegalStateException(this + " is immutable");
}
}
@Override
public void clear() {
checkImmutable();
if (myLongIds != null) {
myLongIds.clear();
}
if (myStringIds != null) {
myStringIds.clear();
}
myModCount++;
}
public Iterable getItemTypes() {
return new Iterable() {
@Override
public Iterator iterator() {
return new TypesIterator();
}
};
}
@NotNull
private Map ensureLongIdsMap(int initialSize) {
if (myLongIds == null) {
myLongIds = new HashMap<>(initialSize);
}
return myLongIds;
}
@NotNull
private Map ensureStringIdsMap(int initialSize) {
if (myStringIds == null) {
myStringIds = new HashMap<>(initialSize);
}
return myStringIds;
}
public ItemIdentitySet copyAllOfType(String typeId) {
ItemIdentitySet r = new ItemIdentitySet();
if (myLongIds != null) {
COWLongSet set = myLongIds.get(typeId);
if (set != null) {
r.myLongIds = new HashMap<>(1);
r.myLongIds.put(typeId, set.copy(r));
}
}
if (myStringIds != null) {
COWStringSet set = myStringIds.get(typeId);
if (set != null) {
r.myStringIds = new HashMap<>(1);
r.myStringIds.put(typeId, set.copy(r));
}
}
return r;
}
public ItemIdentitySet copy() {
ItemIdentitySet r = new ItemIdentitySet();
r.addAll(this);
return r;
}
private WritableLongSet createLongSet(int capacity, Long addElement) {
LongOpenHashSet set = new LongOpenHashSet(capacity);
if (addElement != null) {
set.addAll(addElement);
}
return set;
}
private Set createStringSet(int capacity, String addElement) {
Set set = new HashSet<>(capacity);
if (addElement != null) {
set.add(addElement);
}
return set;
}
public LongSizedIterable longIds(String typeId) {
COWLongSet set = myLongIds == null ? null : myLongIds.get(typeId);
if (set == null) {
return LongList.EMPTY;
}
return set.set();
}
public Set stringIds(String typeId) {
COWStringSet set = myStringIds == null ? null : myStringIds.get(typeId);
if (set == null) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(set.set());
}
@NotNull
public static Set collectItemIds(@NotNull RowRetriever rowRetriever, @Nullable LongIterable rows) {
if (rows == null) return Collections.emptySet();
ItemIdentitySet r = new ItemIdentitySet();
rowRetriever.scanRows(rows, false, ITEM_NOT_NEEDED, IGNORE_MISSING_ROWS, row -> {
r.add(row.getItemId());
return true;
});
return r;
}
private abstract class COWBaseSet> {
@NotNull
private T mySet;
private boolean myCopyOnWrite;
public COWBaseSet(@NotNull T set, boolean copyOnWrite) {
mySet = set;
myCopyOnWrite = copyOnWrite;
}
protected abstract T makeCopy(T set, int additionalSize);
protected abstract S createInstance(T set, boolean copyOnWrite, ItemIdentitySet owner);
public T set() {
return mySet;
}
public S copy(ItemIdentitySet owner) {
T copied = set();
if (isSafeToReuse()) {
return createInstance(copied, true, owner);
} else {
return createInstance(makeCopy(copied, 0), false, owner);
}
}
protected boolean isSafeToReuse() {
return myImmutable || myCopyOnWrite;
}
protected void maybeCopy(int additionalSize) {
if (myCopyOnWrite) {
mySet = makeCopy(mySet, additionalSize);
myCopyOnWrite = false;
}
}
}
private class COWLongSet extends COWBaseSet {
public COWLongSet(@NotNull WritableLongSet set, boolean copyOnWrite) {
super(set, copyOnWrite);
}
public COWLongSet(long id) {
super(createLongSet(SUBSET_INITIAL_CAPACITY, id), false);
}
@Override
protected WritableLongSet makeCopy(WritableLongSet set, int additionalSize) {
WritableLongSet newSet = createLongSet(set.size() + additionalSize, null);
newSet.addAll(set);
return newSet;
}
@Override
protected COWLongSet createInstance(WritableLongSet set, boolean copyOnWrite, ItemIdentitySet owner) {
return owner.new COWLongSet(set, copyOnWrite);
}
public boolean addAll(COWLongSet value) {
maybeCopy(value.set().size());
WritableLongSet set = set();
int sizeBefore = set.size();
set.addAll(value.set());
return sizeBefore != set.size();
}
public boolean add(long id) {
maybeCopy(1);
return set().include(id);
}
public boolean remove(long id) {
maybeCopy(0);
return set().exclude(id);
}
}
private class COWStringSet extends COWBaseSet, COWStringSet> {
public COWStringSet(@NotNull Set set, boolean copyOnWrite) {
super(set, copyOnWrite);
}
public COWStringSet(String id) {
super(createStringSet(SUBSET_INITIAL_CAPACITY, id), false);
}
@Override
protected Set makeCopy(Set set, int additionalSize) {
Set newSet = createStringSet(set.size() + additionalSize, null);
newSet.addAll(set);
return newSet;
}
@Override
protected COWStringSet createInstance(Set set, boolean copyOnWrite, ItemIdentitySet owner) {
return owner.new COWStringSet(set, copyOnWrite);
}
public boolean addAll(COWStringSet value) {
maybeCopy(value.set().size());
return set().addAll(value.set());
}
public boolean add(String id) {
maybeCopy(1);
return set().add(id);
}
public boolean remove(String id) {
maybeCopy(0);
return set().remove(id);
}
}
// todo do we need it writable?
private class RecollectingIterator extends AbstractIterator {
private final int myStartingModCount = myModCount;
// java 8 streams would have been helpful
private Iterator> myLongIdsIterator
= myLongIds == null ? Collections.>emptyIterator() : myLongIds.entrySet().iterator();
private Iterator> myStringIdsIterator
= myStringIds == null ? Collections.>emptyIterator() : myStringIds.entrySet().iterator();
private String myCurrentType;
private LongIterator myCurrentLongIterator;
private Iterator myCurrentStringIterator;
@Override
protected ItemIdentity computeNext() {
if (myModCount != myStartingModCount) {
throw new ConcurrentModificationException();
}
if (myLongIdsIterator != null) {
// iterating longs
if (myCurrentLongIterator != null && myCurrentLongIterator.hasNext()) {
assert myCurrentType != null : this;
return longId(myCurrentType, myCurrentLongIterator.nextValue());
}
while (myLongIdsIterator.hasNext()) {
Map.Entry next = myLongIdsIterator.next();
myCurrentType = next.getKey();
myCurrentLongIterator = next.getValue().set().iterator();
if (myCurrentLongIterator.hasNext()) {
return longId(myCurrentType, myCurrentLongIterator.nextValue());
}
}
myCurrentLongIterator = null;
myLongIdsIterator = null;
}
if (myStringIdsIterator != null) {
// iterating strings
if (myCurrentStringIterator != null && myCurrentStringIterator.hasNext()) {
assert myCurrentType != null : this;
return stringId(myCurrentType, myCurrentStringIterator.next());
}
while (myStringIdsIterator.hasNext()) {
Map.Entry next = myStringIdsIterator.next();
myCurrentType = next.getKey();
myCurrentStringIterator = next.getValue().set().iterator();
if (myCurrentStringIterator.hasNext()) {
return stringId(myCurrentType, myCurrentStringIterator.next());
}
}
myCurrentStringIterator = null;
myStringIdsIterator = null;
}
return endOfData();
}
}
private class TypesIterator extends AbstractIterator {
private final int myStartingModCount = myModCount;
private Iterator myLongKeysIterator
= myLongIds == null ? Collections.emptyIterator() : myLongIds.keySet().iterator();
private Iterator myStringIdsIterator
= myStringIds == null ? Collections.emptyIterator() : myStringIds.keySet().iterator();
@Override
protected String computeNext() {
if (myModCount != myStartingModCount) {
throw new ConcurrentModificationException();
}
if (myLongKeysIterator != null) {
if (myLongKeysIterator.hasNext()) {
return myLongKeysIterator.next();
}
myLongKeysIterator = null;
}
if (myStringIdsIterator != null) {
while (myStringIdsIterator.hasNext()) {
String type = myStringIdsIterator.next();
if (myLongIds == null || !myLongIds.containsKey(type)) {
return type;
}
}
myStringIdsIterator = null;
}
return endOfData();
}
}
}