/******************************************************************************
* 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.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.eclipse.sapphire.internal.NonSuspendableListener;
import org.eclipse.sapphire.modeling.ModelPath;
import org.eclipse.sapphire.modeling.ModelPath.AllDescendentsSegment;
import org.eclipse.sapphire.modeling.ModelPath.PropertySegment;
import org.eclipse.sapphire.modeling.ModelPath.TypeFilterSegment;
import org.eclipse.sapphire.util.EqualsFactory;
import org.eclipse.sapphire.util.HashCodeFactory;
/**
* @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
*/
public final class ElementList<T extends Element> extends Property implements List<T>
{
private static final Comparator<String> DEFAULT_COMPARATOR = new Comparator<String>()
{
@Override
public int compare( final String str1, final String str2 )
{
return str1.compareTo( str2 );
}
};
private List<T> content;
private Map<IndexCacheKey,Index<T>> indexes;
public ElementList( final Element element, final ListProperty property )
{
super( element, property );
}
/**
* Returns a reference to ElementList.class that is parameterized with the given type.
*
* <p>Example:</p>
*
* <p><code>Class<ElementList<Item>> cl = ElementList.of( Item.class );</code></p>
*
* @param type the type
* @return a reference to ElementList.class that is parameterized with the given type
*/
@SuppressWarnings( { "unchecked", "rawtypes" } )
public static <TX extends Element> Class<ElementList<TX>> of( final Class<TX> type )
{
return (Class) ElementList.class;
}
@Override
public void refresh()
{
synchronized( root() )
{
init();
refreshContent( false );
if( this.content != null )
{
for( T element : this.content )
{
element.refresh();
}
}
refreshEnablement( false );
refreshValidation( false );
}
}
private void refreshContent( final boolean onlyIfNotInitialized )
{
boolean initialized = ( ( this.initialization & CONTENT_INITIALIZED ) != 0 );
if( ! initialized || ! onlyIfNotInitialized )
{
final ListPropertyBinding binding = binding();
final List<? extends Resource> freshResources = binding.read();
final int freshContentSize = freshResources.size();
boolean proceed;
initialized = ( ( this.initialization & CONTENT_INITIALIZED ) != 0 );
if( initialized )
{
if( this.content.size() == freshContentSize )
{
proceed = false;
for( int i = 0; i < freshContentSize; i++ )
{
if( this.content.get( i ).resource() != freshResources.get( i ) )
{
proceed = true;
break;
}
}
}
else
{
proceed = true;
}
}
else
{
proceed = true;
}
if( proceed )
{
final List<T> freshContent = new ArrayList<T>( freshContentSize );
for( Resource resource : freshResources )
{
T element = null;
if( this.content != null )
{
for( T x : this.content )
{
if( resource == x.resource() )
{
element = x;
break;
}
}
}
if( element == null )
{
final ElementType type = binding.type( resource );
element = type.instantiate( this, resource );
}
freshContent.add( element );
}
final List<T> toBeDisposed = new ArrayList<T>( 1 );
if( this.content != null )
{
for( T x : this.content )
{
boolean retained = false;
for( T y : freshContent )
{
if( x == y )
{
retained = true;
break;
}
}
if( ! retained )
{
toBeDisposed.add( x );
}
}
}
PropertyContentEvent event = null;
this.content = freshContent;
if( initialized )
{
event = new PropertyContentEvent( this );
}
else
{
this.initialization |= CONTENT_INITIALIZED;
}
for( Element x : toBeDisposed )
{
try
{
x.dispose();
}
catch( Exception e )
{
Sapphire.service( LoggingService.class ).log( e );
}
}
broadcast( event );
}
}
}
@Override
public ListProperty definition()
{
return (ListProperty) super.definition();
}
@Override
protected ListPropertyBinding binding()
{
return (ListPropertyBinding) super.binding();
}
@Override
public void attach( final Listener listener, final ModelPath path )
{
if( listener == null )
{
throw new IllegalArgumentException();
}
if( path == null )
{
throw new IllegalArgumentException();
}
synchronized( root() )
{
assertNotDisposed();
if( path.length() > 0 )
{
final ModelPath.Segment head = path.head();
if( head instanceof AllDescendentsSegment || head instanceof PropertySegment || head instanceof TypeFilterSegment )
{
attach( listener );
attach( new PropagationListener( listener, path ) );
for( Element element : this )
{
element.attach( listener, path );
}
return;
}
}
super.attach( listener, path );
}
}
@Override
public void detach( final Listener listener, final ModelPath path )
{
if( listener == null )
{
throw new IllegalArgumentException();
}
if( path == null )
{
throw new IllegalArgumentException();
}
synchronized( root() )
{
if( path.length() > 0 )
{
final ModelPath.Segment head = path.head();
if( head instanceof AllDescendentsSegment || head instanceof PropertySegment || head instanceof TypeFilterSegment )
{
detach( listener );
detach( new PropagationListener( listener, path ) );
for( Element element : this )
{
element.detach( listener, path );
}
return;
}
}
super.detach( listener, path );
}
}
public T insert()
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return insert$( (ElementType) null, size$() );
}
}
public T insert( final int position )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return insert$( (ElementType) null, position );
}
}
public T insert( final ElementType type )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return insert$( type, size$() );
}
}
public <C extends Element> C insert( final Class<C> cl )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return insert$( cl, size$() );
}
}
public T insert( final ElementType type, final int position )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return insert$( type, position );
}
}
private T insert$( final ElementType type, final int position )
{
final Set<ElementType> possible = service( PossibleTypesService.class ).types();
ElementType t = type;
if( t == null )
{
if( possible.size() > 1 )
{
throw new IllegalArgumentException();
}
t = possible.iterator().next();
}
else if( ! possible.contains( t ) )
{
throw new IllegalArgumentException();
}
final Resource resource = binding().insert( t, position );
refresh();
T element = null;
for( T x : this.content )
{
if( x.resource() == resource )
{
element = x;
element.initialize();
break;
}
}
if( element == null )
{
throw new IllegalStateException();
}
return element;
}
public <C extends Element> C insert( final Class<C> cl, final int position )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return insert$( cl, position );
}
}
@SuppressWarnings( "unchecked" )
private <C extends Element> C insert$( final Class<C> cl, final int position )
{
ElementType type = null;
if( cl != null )
{
type = ElementType.read( cl );
if( type == null )
{
throw new IllegalArgumentException();
}
}
return (C) insert$( type, position );
}
@Override
public void copy( final Element source )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
if( source == null )
{
throw new IllegalArgumentException();
}
final Property p = source.property( name() );
if( p instanceof ElementList )
{
clear$();
final Set<ElementType> possibleTypes = service( PossibleTypesService.class ).types();
for( Element sourceChildElement : (ElementList<?>) p )
{
final ElementType sourceChildElementType = sourceChildElement.type();
if( possibleTypes.contains( sourceChildElementType ) )
{
final Element targetChildElement = insert$( sourceChildElement.type(), size$() );
targetChildElement.copy( sourceChildElement );
}
}
}
}
}
@Override
public void copy( final ElementData source )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
if( source == null )
{
throw new IllegalArgumentException();
}
final Object content = source.read( name() );
clear$();
if( content instanceof List )
{
final Set<ElementType> possibleTypes = service( PossibleTypesService.class ).types();
for( final Object item : (List<?>) content )
{
final ElementData sourceChildElementData = (ElementData) item ;
final ElementType sourceChildElementType = sourceChildElementData.type();
if( possibleTypes.contains( sourceChildElementType ) )
{
final Element targetChildElement = insert$( sourceChildElementData.type(), size$() );
targetChildElement.copy( sourceChildElementData );
}
}
}
}
}
public void move( final Element element, final int position )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
if( position < 0 || position > size() )
{
throw new IllegalArgumentException();
}
final int oldPosition = indexOf$( element );
if( oldPosition == -1 )
{
throw new IllegalArgumentException();
}
if( position != oldPosition )
{
binding().move( element.resource(), position );
refresh();
}
}
}
public void moveUp( final Element element )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
final int index = indexOf$( element );
if( index == -1 )
{
throw new IllegalArgumentException();
}
if( index > 0 )
{
final T previous = this.content.get( index - 1 );
swap$( element, previous );
}
}
}
public void moveDown( final Element element )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
final int index = indexOf$( element );
if( index == -1 )
{
throw new IllegalArgumentException();
}
if( index < this.content.size() - 1 )
{
final T next = this.content.get( index + 1 );
swap$( element, next );
}
}
}
public void swap( final Element a, final Element b )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
swap$( a, b );
}
}
private void swap$( final Element a, final Element b )
{
final int aPosition = indexOf$( a );
final int bPosition = indexOf$( b );
if( aPosition == -1 || bPosition == -1 )
{
throw new IllegalArgumentException();
}
if( aPosition != bPosition )
{
final ListPropertyBinding binding = binding();
binding.move( a.resource(), bPosition );
binding.move( b.resource(), aPosition );
refresh();
}
}
public boolean remove( final Object object )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
return remove$( object );
}
}
private boolean remove$( final Object object )
{
if( contains$( object ) )
{
final Resource resource = ( (Element) object ).resource();
binding().remove( resource );
refresh();
return true;
}
return false;
}
public T remove( final int index )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
final T element = this.content.get( index );
remove$( element );
return element;
}
}
public boolean removeAll( final Collection<?> collection )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
boolean changed = false;
for( Object object : collection )
{
changed = remove$( object ) || changed;
}
return changed;
}
}
public boolean retainAll( final Collection<?> collection )
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
boolean changed = false;
for( T element : this )
{
if( ! collection.contains( element ) )
{
changed = remove$( element ) || changed;
}
}
return changed;
}
}
@Override
public void clear()
{
synchronized( root() )
{
init();
refreshContent( true );
ensureNotReadOnly();
clear$();
}
}
private void clear$()
{
for( T element : this.content )
{
remove$( element );
}
}
public T get( final int index )
{
synchronized( root() )
{
init();
refreshContent( true );
return this.content.get( index );
}
}
public int indexOf( final Object object )
{
synchronized( root() )
{
init();
refreshContent( true );
return indexOf$( object );
}
}
private int indexOf$( final Object object )
{
int index = -1;
for( int i = 0, n = this.content.size(); i < n; i++ )
{
if( this.content.get( i ) == object )
{
index = i;
break;
}
}
return index;
}
public int lastIndexOf( final Object object )
{
synchronized( root() )
{
init();
refreshContent( true );
int index = -1;
for( int i = 0, n = this.content.size(); i < n; i++ )
{
if( this.content.get( i ) == object )
{
index = i;
}
}
return index;
}
}
public boolean contains( final Object object )
{
synchronized( root() )
{
init();
refreshContent( true );
return contains$( object );
}
}
private boolean contains$( final Object object )
{
for( Object x : this.content )
{
if( x == object )
{
return true;
}
}
return false;
}
public boolean containsAll( final Collection<?> collection )
{
synchronized( root() )
{
init();
refreshContent( true );
for( Object x : collection )
{
if( ! contains$( x ) )
{
return false;
}
}
return true;
}
}
public boolean isEmpty()
{
synchronized( root() )
{
init();
refreshContent( true );
return this.content.isEmpty();
}
}
@Override
public boolean empty()
{
synchronized( root() )
{
init();
refreshContent( true );
return this.content.isEmpty();
}
}
public int size()
{
synchronized( root() )
{
init();
refreshContent( true );
return size$();
}
}
private int size$()
{
return this.content.size();
}
public Iterator<T> iterator()
{
synchronized( root() )
{
init();
refreshContent( true );
return new Itr<T>( this.content.iterator() );
}
}
public ListIterator<T> listIterator()
{
synchronized( root() )
{
init();
refreshContent( true );
return new ListItr<T>( this.content.listIterator() );
}
}
public ListIterator<T> listIterator( final int index )
{
synchronized( root() )
{
init();
refreshContent( true );
return new ListItr<T>( this.content.listIterator( index ) );
}
}
public List<T> subList( final int fromIndex,
final int toIndex )
{
throw new UnsupportedOperationException();
}
public Object[] toArray()
{
synchronized( root() )
{
init();
refreshContent( true );
return this.content.toArray();
}
}
public <E> E[] toArray( E[] array )
{
synchronized( root() )
{
init();
refreshContent( true );
return this.content.toArray( array );
}
}
public boolean add( final T object )
{
throw new UnsupportedOperationException();
}
public void add( final int index, final T element )
{
throw new UnsupportedOperationException();
}
public boolean addAll( final Collection<? extends T> collection )
{
throw new UnsupportedOperationException();
}
public boolean addAll( final int index, final Collection<? extends T> collection )
{
throw new UnsupportedOperationException();
}
public T set( final int index, final T element )
{
throw new UnsupportedOperationException();
}
/**
* Returns an index with the specified value property as the key. The index is created if it doesn't exist already.
*
* @param property the key property
* @return the index
* @throws IllegalArgumentException if the property is null; if the property does not belong to the list entry type
*/
public Index<T> index( final ValueProperty property )
{
if( property == null )
{
throw new IllegalArgumentException();
}
return index( property.name(), null );
}
/**
* Returns an index with the specified value property as the key. The index is created if it doesn't exist already.
*
* @param property the key property
* @param comparator the comparator to use when looking up elements in the index
* @return the index
* @throws IllegalArgumentException if the property is null; if the property does not belong to the list entry type
*/
public Index<T> index( final ValueProperty property, final Comparator<String> comparator )
{
if( property == null )
{
throw new IllegalArgumentException();
}
return index( property.name(), comparator );
}
/**
* Returns an index with the specified value property as the key. The index is created if it doesn't exist already.
*
* <p>The returned index will treat keys that differ only on letter case as different.</p>
*
* @param property the key property
* @return the index
* @throws IllegalArgumentException if the property is null; if the property does not exist in the list entry type;
* if the property is a path; if the property is not a value property
*/
public Index<T> index( final String property )
{
if( property == null )
{
throw new IllegalArgumentException();
}
return index( property, null );
}
/**
* Returns an index with the specified value property as the key. The index is created if it doesn't exist already.
*
* @param property the key property
* @param comparator the comparator to use when looking up elements in the index
* @return the index
* @throws IllegalArgumentException if the property is null; if the property does not exist in the list entry type;
* if the property is a path; if the property is not a value property
*/
public Index<T> index( final String property, final Comparator<String> comparator )
{
if( property == null )
{
throw new IllegalArgumentException();
}
if( property.indexOf( '/' ) != -1 )
{
throw new IllegalArgumentException();
}
final ElementType entryType = definition().getType();
final PropertyDef p = entryType.property( property );
if( p == null )
{
throw new IllegalArgumentException();
}
if( ! ( p instanceof ValueProperty ) )
{
throw new IllegalArgumentException();
}
final ValueProperty vp = (ValueProperty) p;
final Comparator<String> c = ( comparator == null ? DEFAULT_COMPARATOR : comparator );
synchronized( root() )
{
assertNotDisposed();
final IndexCacheKey key = new IndexCacheKey( vp, c );
if( this.indexes == null )
{
this.indexes = new HashMap<IndexCacheKey,Index<T>>();
}
Index<T> index = this.indexes.get( key );
if( index == null )
{
index = new Index<T>( this, vp, c );
this.indexes.put( key, index );
}
return index;
}
}
@Override
protected void disposeOther()
{
if( this.content != null )
{
for( final T element : this.content )
{
element.dispose();
}
this.content = null;
}
this.indexes = null;
}
private void ensureNotReadOnly()
{
if( definition().isReadOnly() )
{
throw new UnsupportedOperationException();
}
}
private static final class PropagationListener extends FilteredListener<PropertyContentEvent> implements NonSuspendableListener
{
private final Listener listener;
private final ModelPath path;
public PropagationListener( final Listener listener, final ModelPath path )
{
this.listener = listener;
this.path = path;
}
@Override
public boolean equals( final Object obj )
{
if( obj instanceof PropagationListener )
{
final PropagationListener pl = (PropagationListener) obj;
return this.listener.equals( pl.listener ) && this.path.equals( pl.path );
}
return false;
}
@Override
public int hashCode()
{
return this.listener.hashCode() ^ this.path.hashCode();
}
@Override
protected void handleTypedEvent( final PropertyContentEvent event )
{
for( Element element : (ElementList<?>) event.property() )
{
element.attach( this.listener, this.path );
}
}
}
private static class Itr<T> implements Iterator<T>
{
private final Iterator<T> baseIterator;
public Itr( final Iterator<T> baseIterator )
{
this.baseIterator = baseIterator;
}
public boolean hasNext()
{
return this.baseIterator.hasNext();
}
public T next()
{
return this.baseIterator.next();
}
public void remove()
{
throw new UnsupportedOperationException();
}
}
private static final class ListItr<T> extends Itr<T> implements ListIterator<T>
{
private final ListIterator<T> baseIterator;
public ListItr( final ListIterator<T> baseIterator )
{
super( baseIterator );
this.baseIterator = baseIterator;
}
public int nextIndex()
{
return this.baseIterator.nextIndex();
}
public boolean hasPrevious()
{
return this.baseIterator.hasPrevious();
}
public T previous()
{
return this.baseIterator.previous();
}
public int previousIndex()
{
return this.baseIterator.previousIndex();
}
public void add( final T object )
{
throw new UnsupportedOperationException();
}
public void set( final T object )
{
throw new UnsupportedOperationException();
}
}
private static final class IndexCacheKey
{
private final ValueProperty property;
private final Comparator<String> comparator;
public IndexCacheKey( final ValueProperty property, final Comparator<String> comparator )
{
this.property = property;
this.comparator = comparator;
}
@Override
public int hashCode()
{
return HashCodeFactory
.start()
.add( this.property.name() )
.add( this.comparator )
.result();
}
@Override
public boolean equals( final Object obj )
{
if( obj instanceof IndexCacheKey )
{
final IndexCacheKey key = (IndexCacheKey) obj;
return EqualsFactory
.start()
.add( this.property, key.property )
.add( this.comparator, key.comparator )
.result();
}
return false;
}
}
}