/****************************************************************************** * Copyright (c) 2016 Oracle * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Konstantin Komissarchik - initial implementation and ongoing maintenance ******************************************************************************/ package org.eclipse.sapphire; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import org.eclipse.sapphire.util.IdentityHashSet; import org.eclipse.sapphire.util.SetFactory; /** * An index provides an efficient way to lookup elements in a list by a property value. * * <p>To create an index, use the {@link ElementList#index(ValueProperty)} method.</p> * * @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a> */ public final class Index<T extends Element> { @Text( "{0} property is already disposed." ) private static LocalizableText propertyAlreadyDisposed; static { LocalizableText.init( Index.class ); } private static final Object NULL = new Object(); private final ElementList<T> list; private final ValueProperty property; private final Comparator<String> comparator; private Map<Object,Object> keyToElements; private Map<Element,Object> elementToKey; private Listener listener; private ListenerContext listeners; Index( final ElementList<T> list, final ValueProperty property, final Comparator<String> comparator ) { if( list == null ) { throw new IllegalArgumentException(); } if( property == null ) { throw new IllegalArgumentException(); } if( comparator == null ) { throw new IllegalArgumentException(); } this.list = list; this.property = property; this.comparator = comparator; } private void initialize() { if( this.keyToElements == null ) { this.listener = new FilteredListener<PropertyContentEvent>() { @Override protected void handleTypedEvent( final PropertyContentEvent event ) { Index.this.handle( event ); } }; this.list.attach( this.listener ); Comparator<Object> comparator = null; if( this.comparator != null ) { comparator = new Comparator<Object>() { @Override public int compare( final Object x, final Object y ) { if( x == y ) { return 0; } else if( x == NULL ) { return -1; } else if( y == NULL ) { return 1; } else { return Index.this.comparator.compare( (String) x, (String) y ); } } }; } this.keyToElements = new TreeMap<Object,Object>( comparator ); this.elementToKey = new IdentityHashMap<Element,Object>(); for( final Element element : this.list ) { insert( element ); element.attach( this.listener ); element.property( this.property ).attach( this.listener ); } } } /** * Returns the indexed list. * * @return the indexed list */ public ElementList<?> list() { return this.list; } /** * Returns the property that is used as the key by this index. * * @return the property that is used as the key by this index */ public ValueProperty property() { return this.property; } /** * Returns an element corresponding to the given key. If multiple elements are found, no guarantees * are made as to which of these elements will be returned. * * @param key the key to use for the lookup * @return an element corresponding to the given key or null * @throws IllegalStateException if the list property is disposed */ @SuppressWarnings( "unchecked" ) public T element( final String key ) { synchronized( this.list.root() ) { assertNotDisposed(); initialize(); final Object obj = this.keyToElements.get( key == null ? NULL : key ); if( obj != null ) { if( obj instanceof Element ) { return (T) obj; } else { return ( (Set<T>) obj ).iterator().next(); } } } return null; } /** * Returns all the elements corresponding to the given key. * * @param key the key to use for the lookup * @return all the element corresponding to the given key or an empty set * @throws IllegalStateException if the list property is disposed */ @SuppressWarnings( "unchecked" ) public Set<T> elements( final String key ) { synchronized( this.list.root() ) { assertNotDisposed(); initialize(); final Object obj = this.keyToElements.get( key == null ? NULL : key ); if( obj != null ) { if( obj instanceof Element ) { return SetFactory.singleton( (T) obj ); } else { return Collections.unmodifiableSet( new IdentityHashSet<T>( (Set<T>) obj ) ); } } } return SetFactory.empty(); } /** * Attaches a listener to this index. * * @param listener the listener * @throws IllegalArgumentException if the listener is null * @throws IllegalStateException if the list property is disposed */ public void attach( final Listener listener ) { if( listener == null ) { throw new IllegalArgumentException(); } synchronized( this.list.root() ) { assertNotDisposed(); if( this.listeners == null ) { this.listeners = new ListenerContext( ( (ElementImpl) this.list.element() ).queue() ); } this.listeners.attach( listener ); } } /** * Detaches a listener from this index. * * @param listener the listener * @throws IllegalArgumentException if the listener is null */ public void detach( final Listener listener ) { if( listener == null ) { throw new IllegalArgumentException(); } synchronized( this.list.root() ) { if( this.listeners != null ) { this.listeners.detach( listener ); } } } private void handle( final PropertyContentEvent event ) { synchronized( this.list.root() ) { boolean changed = false; final Property property = event.property(); if( property instanceof Value ) { final Element element = property.element(); remove( element ); insert( element ); changed = true; } else { for( final Element element : this.list ) { if( ! this.elementToKey.containsKey( element ) ) { insert( element ); element.attach( this.listener ); element.property( this.property ).attach( this.listener ); changed = true; } } List<Element> disposed = null; for( final Element element : this.elementToKey.keySet() ) { if( element.disposed() ) { if( disposed == null ) { disposed = new ArrayList<Element>( 1 ); } disposed.add( element ); } } if( disposed != null ) { for( final Element element : disposed ) { remove( element ); } changed = true; } } if( changed ) { if( this.listeners != null ) { this.listeners.broadcast( new Event() ); } } } } private void insert( final Element element ) { if( element == null ) { throw new IllegalStateException(); } Object key = element.property( this.property ).text(); if( key == null ) { key = NULL; } final Object object = this.keyToElements.get( key ); if( object == null ) { this.keyToElements.put( key, element ); } else if( object instanceof Element ) { final Set<Element> set = new IdentityHashSet<Element>(); set.add( (Element) object ); set.add( element ); this.keyToElements.put( key, set ); } else { @SuppressWarnings( "unchecked" ) final Set<Element> set = (Set<Element>) object; set.add( element ); } this.elementToKey.put( element, key ); } private void remove( final Element element ) { if( element == null ) { throw new IllegalStateException(); } final Object key = this.elementToKey.remove( element ); if( key != null ) { final Object object = this.keyToElements.get( key ); if( object != null ) { if( object instanceof Element ) { this.keyToElements.remove( key ); } else { final Set<?> set = (Set<?>) object; set.remove( element ); if( set.size() == 1 ) { this.keyToElements.put( key, set.iterator().next() ); } } } } } private void assertNotDisposed() { if( this.list.disposed() ) { final String msg = propertyAlreadyDisposed.format( this.list.name() ); throw new IllegalStateException( msg ); } } }