/******************************************************************************
* Copyright (c) 2014 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.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.sapphire.internal.NonSuspendableListener;
import org.eclipse.sapphire.modeling.ElementDisposeEvent;
import org.eclipse.sapphire.modeling.ElementEvent;
import org.eclipse.sapphire.modeling.ModelPath;
import org.eclipse.sapphire.modeling.ModelPath.AllDescendentsSegment;
import org.eclipse.sapphire.modeling.ModelPath.ModelRootSegment;
import org.eclipse.sapphire.modeling.ModelPath.ParentElementSegment;
import org.eclipse.sapphire.modeling.Status;
import org.eclipse.sapphire.modeling.annotations.ClearOnDisable;
import org.eclipse.sapphire.services.DependenciesService;
import org.eclipse.sapphire.services.Service;
import org.eclipse.sapphire.services.ValidationService;
import org.eclipse.sapphire.services.internal.PropertyInstanceServiceContext;
/**
* Represents an instance of a property within an element.
*
* @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
*/
public abstract class Property implements Observable
{
private static final int INITIALIZED = 1;
private static final int ENABLEMENT_INITIALIZED = 1 << 1;
private static final int VALIDATION_INITIALIZED = 1 << 2;
protected static final int CONTENT_INITIALIZED = 1 << 3;
@Text( "{0} property is already disposed." )
private static LocalizableText propertyAlreadyDisposed;
@Text( "Path \"{2}\" is invalid for {0}#{1}." )
private static LocalizableText illegalPathException;
static
{
LocalizableText.init( Property.class );
}
private final Element element;
private final PropertyDef definition;
private PropertyInstanceServiceContext services;
private ListenerContext listeners;
private boolean enablement;
private Status validation;
protected byte initialization;
private boolean disposed = false;
public Property( final Element element, final PropertyDef property )
{
if( element == null )
{
throw new IllegalArgumentException();
}
this.element = element;
if( property == null )
{
throw new IllegalArgumentException();
}
this.definition = property;
}
protected final void init()
{
assertNotDisposed();
if( ( this.initialization & INITIALIZED ) == 0 )
{
this.initialization |= INITIALIZED;
for( Listener listener : definition().listeners() )
{
attach( listener );
}
final Listener triggerRefreshListener = new Listener()
{
@Override
public void handle( final Event event )
{
if( ! disposed() )
{
refresh();
}
}
};
final Set<ModelPath> dependencies = new HashSet<ModelPath>();
for( DependenciesService ds : services( DependenciesService.class ) )
{
dependencies.addAll( ds.dependencies() );
}
if( ! dependencies.isEmpty() )
{
for( ModelPath dependency : dependencies )
{
element().attach( triggerRefreshListener, dependency );
}
final Listener disposeListener = new FilteredListener<ElementDisposeEvent>()
{
@Override
protected void handleTypedEvent( final ElementDisposeEvent event )
{
for( ModelPath dependency : dependencies )
{
element().detach( triggerRefreshListener, dependency );
}
}
};
element().attach( disposeListener );
}
}
}
protected final void refreshEnablement( final boolean onlyIfNotInitialized )
{
boolean initialized;
synchronized( this )
{
initialized = ( ( this.initialization & ENABLEMENT_INITIALIZED ) != 0 );
}
if( ! initialized || ! onlyIfNotInitialized )
{
boolean after = true;
if( ! initialized )
{
final Listener listener = new Listener()
{
@Override
public void handle( final Event event )
{
refreshEnablement( false );
}
};
for( EnablementService service : services( EnablementService.class ) )
{
service.attach( listener );
}
if( definition().hasAnnotation( ClearOnDisable.class ) )
{
final Listener clearOnDisableListener = new FilteredListener<PropertyEnablementEvent>()
{
@Override
protected void handleTypedEvent( final PropertyEnablementEvent event )
{
if( event.before() == true && event.after() == false )
{
clear();
}
}
};
attach( clearOnDisableListener );
}
}
for( EnablementService service : services( EnablementService.class ) )
{
after = ( after && service.enablement() );
if( after == false )
{
break;
}
}
PropertyEnablementEvent event = null;
synchronized( this )
{
initialized = ( ( this.initialization & ENABLEMENT_INITIALIZED ) != 0 );
if( initialized )
{
final boolean before = this.enablement;
if( before != after )
{
this.enablement = after;
event = new PropertyEnablementEvent( this, before, after );
}
}
else
{
this.enablement = after;
this.initialization |= ENABLEMENT_INITIALIZED;
}
}
broadcast( event );
}
}
protected final void refreshValidation( final boolean onlyIfNotInitialized )
{
boolean initialized;
synchronized( this )
{
initialized = ( ( this.initialization & VALIDATION_INITIALIZED ) != 0 );
}
if( ! initialized || ! onlyIfNotInitialized )
{
final Status.CompositeStatusFactory freshValidationResultFactory = Status.factoryForComposite();
if( ! initialized )
{
final Listener listener = new Listener()
{
@Override
public void handle( final Event event )
{
refreshValidation( false );
}
};
for( final ValidationService service : services( ValidationService.class ) )
{
service.attach( listener );
}
}
for( final ValidationService service : services( ValidationService.class ) )
{
freshValidationResultFactory.merge( service.validation() );
}
final Status freshValidationResult = freshValidationResultFactory.create();
PropertyValidationEvent event = null;
synchronized( this )
{
initialized = ( ( this.initialization & VALIDATION_INITIALIZED ) != 0 );
if( initialized )
{
final Status staleValidationResult = this.validation;
if( ! staleValidationResult.equals( freshValidationResult ) )
{
this.validation = freshValidationResult;
event = new PropertyValidationEvent( this, staleValidationResult, freshValidationResult );
}
}
else
{
this.validation = freshValidationResult;
this.initialization |= VALIDATION_INITIALIZED;
}
}
broadcast( event );
}
}
/**
* Returns the root of the model.
*
* @return the root of the model
*/
public final Element root()
{
return this.element.root();
}
/**
* Return the element instance.
*
* @return the element instance
*/
public final Element element()
{
return this.element;
}
/**
* Determines whether an element is located within a model tree that has this property as the root. Always returns
* false if this property is a value or a transient property.
*
* @param element the element
* @return true if the element is contained by this property and false otherwise
*/
public boolean holds( final Element element )
{
if( element == null )
{
throw new IllegalArgumentException();
}
for( Property p = element.parent(); p != null; p = p.element().parent() )
{
if( this == p )
{
return true;
}
}
return false;
}
/**
* Determines whether a property is located within a model tree that has this property as the root.
*
* @param property the property
* @return true if the property is contained by this property and false otherwise
*/
public boolean holds( final Property property )
{
if( property == null )
{
throw new IllegalArgumentException();
}
for( Property p = property; p != null; p = p.element().parent() )
{
if( this == p )
{
return true;
}
}
return false;
}
/**
* Returns the property definition.
*
* @return the property definition
*/
public PropertyDef definition()
{
return this.definition;
}
/**
* Returns the property name.
*
* @return the property name
*/
public final String name()
{
return this.definition.name();
}
public final <T> T nearest( final Class<T> type )
{
if( type.isAssignableFrom( getClass() ) )
{
return type.cast( this );
}
else
{
return element().nearest( type );
}
}
protected PropertyBinding binding()
{
return element().resource().binding( this );
}
/**
* Clears this property.
*/
public abstract void clear();
/**
* Copies property content from the provided source element. The source element does not have to
* be of the same type as target. The copy will happen if the source element has a property with
* the same name and type as this property. Otherwise, no change will be performed.
*
* @param source the element to copy from
* @throws IllegalArgumentException if source is null
* @throws UnsupportedOperationException if this property is read-only
* @throws IllegalStateException if this property or the source element is already disposed
*/
public abstract void copy( Element source );
/**
* Copies property content from the provided source element data. The source element data does not
* have to be of the same type as target. Any property that is not found in source or is of the wrong
* type, will be cleared in target.
*
* @since 8.1
* @param source the element to copy from
* @throws IllegalArgumentException if source is null
* @throws UnsupportedOperationException if this property is read-only
* @throws IllegalStateException if this property is already disposed
*/
public abstract void copy( ElementData source );
/**
* Determines if this property is empty. The empty state is defined as follows:
*
* <ul>
* <li><b>Value Property</b> - has null value or has default value</li>
* <li><b>Element Property</b> - element does not exist</li>
* <li><b>Implied Element Property</b> - none of the child element's properties are non-empty</li>
* <li><b>List Property</b> - list size is zero</li>
* <li><b>Transient Property</b> - has null content</li>
* </ul>
*
* @return true if this property is empty, false otherwise
* @throws IllegalStateException if this property is already disposed
*/
public abstract boolean empty();
/**
* Determines whether this property is enabled
*
* @return true if this property is enabled and false otherwise
* @throws IllegalStateException if this property is already disposed
*/
public final boolean enabled()
{
init();
refreshEnablement( true );
synchronized( this )
{
return this.enablement;
}
}
/**
* Returns the validation result for this property.
*
* @return the validation result for this property
* @throws IllegalStateException if this property is already disposed
*/
public final Status validation()
{
init();
refreshValidation( true );
synchronized( this )
{
return this.validation;
}
}
public abstract void refresh();
/**
* Returns the service of the specified type from the property instance service context.
*
* <p>Service Context: <b>Sapphire.Property.Instance</b></p>
*
* @param <S> the type of the service
* @param type the type of the service
* @return the service or <code>null</code> if not available
*/
public final <S extends Service> S service( final Class<S> type )
{
assertNotDisposed();
if( type == null )
{
throw new IllegalArgumentException();
}
final List<S> services = services( type );
return ( services.isEmpty() ? null : services.get( 0 ) );
}
/**
* Returns the service of the specified type from the property instance service context.
*
* <p>Service Context: <b>Sapphire.Property.Instance</b></p>
*
* @param <S> the type of the service
* @param type the type of the service
* @return the service or <code>null</code> if not available
*/
public final <S extends Service> List<S> services( final Class<S> type )
{
assertNotDisposed();
if( type == null )
{
throw new IllegalArgumentException();
}
synchronized( root() )
{
if( this.services == null )
{
this.services = new PropertyInstanceServiceContext( this, ( (ElementImpl) element() ).queue() );
}
return this.services.services( type );
}
}
private ListenerContext listeners( final boolean createIfNecessary )
{
final Element root = root();
synchronized( root )
{
if( this.listeners == null && createIfNecessary )
{
assertNotDisposed();
this.listeners = new ListenerContext( ( (ElementImpl) root ).queue() );
}
return this.listeners;
}
}
/**
* Attaches a listener to this property.
*
* @param listener the listener
* @throws IllegalArgumentException if the listener is null
* @throws IllegalStateException if this property is disposed
*/
public final void attach( final Listener listener )
{
if( listener == null )
{
throw new IllegalArgumentException();
}
listeners( true ).attach( listener );
}
/**
* Attaches a listener to this property.
*
* @param listener the listener
* @param path
* @throws IllegalArgumentException if the listener is null
* @throws IllegalArgumentException if the path is null or invalid
* @throws IllegalStateException if this property is disposed
*/
public final void attach( final Listener listener, final String path )
{
if( listener == null )
{
throw new IllegalArgumentException();
}
if( path == null )
{
throw new IllegalArgumentException();
}
synchronized( root() )
{
assertNotDisposed();
attach( listener, new ModelPath( path ) );
}
}
/**
* Attaches a listener to this property.
*
* @param listener the listener
* @param path
* @throws IllegalArgumentException if the listener is null
* @throws IllegalArgumentException if the path is null or invalid
* @throws IllegalStateException if this property is disposed
*/
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 )
{
attach( listener );
}
else
{
final ModelPath.Segment head = path.head();
if( head instanceof AllDescendentsSegment )
{
attach( listener );
}
else if( head instanceof ModelRootSegment )
{
root().attach( listener, path.tail() );
}
else if( head instanceof ParentElementSegment )
{
final Property parent = element().parent();
if( parent == null )
{
throw createIllegalPathException( path );
}
parent.element().attach( listener, path.tail() );
}
else
{
throw createIllegalPathException( path );
}
}
}
}
/**
* Detaches a listener from this property.
*
* @param listener the listener
* @throws IllegalArgumentException if the listener is null
*/
public final void detach( final Listener listener )
{
if( listener == null )
{
throw new IllegalArgumentException();
}
final ListenerContext listeners = listeners( false );
if( listeners != null )
{
listeners.detach( listener );
}
}
/**
* Detaches a listener from this property.
*
* @param listener the listener
* @param path
* @throws IllegalArgumentException if the listener is null
* @throws IllegalArgumentException if the path is null or invalid
*/
public final void detach( final Listener listener, final String path )
{
if( listener == null )
{
throw new IllegalArgumentException();
}
if( path == null )
{
throw new IllegalArgumentException();
}
synchronized( root() )
{
detach( listener, new ModelPath( path ) );
}
}
/**
* Detaches a listener from this property.
*
* @param listener the listener
* @param path
* @throws IllegalArgumentException if the listener is null
* @throws IllegalArgumentException if the path is null or invalid
*/
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 )
{
detach( listener );
}
else
{
final ModelPath.Segment head = path.head();
if( head instanceof AllDescendentsSegment )
{
detach( listener );
}
else if( head instanceof ModelRootSegment )
{
root().detach( listener, path.tail() );
}
else if( head instanceof ParentElementSegment )
{
final Property parent = element().parent();
if( parent == null )
{
throw createIllegalPathException( path );
}
parent.element().detach( listener, path.tail() );
}
else
{
throw createIllegalPathException( path );
}
}
}
}
protected final void broadcast( final Event event )
{
if( event != null )
{
final ListenerContext listeners = listeners( false );
if( listeners != null )
{
listeners.broadcast( event );
}
}
}
/**
* Suspends all events related to this property and everything beneath it in the model tree. The suspended
* events will be delivered when the suspension is released.
*
* @return a handle that must be used to release the event suspension
*/
public final Disposable suspend()
{
final JobQueue<EventDeliveryJob> queue = listeners( true ).queue();
final Disposable suspension = queue.suspend( new SuspendFilter() );
return new Disposable()
{
@Override
public void dispose()
{
suspension.dispose();
queue.process();
}
};
}
public final boolean disposed()
{
synchronized( root() )
{
return this.disposed;
}
}
/**
* Only to be called by the framework.
*/
final void dispose()
{
synchronized( root() )
{
if( ! this.disposed )
{
this.disposed = true;
if( this.services != null )
{
this.services.dispose();
this.services = null;
}
disposeOther();
this.listeners = null;
this.validation = null;
}
}
}
protected void disposeOther()
{
// To be overridden.
}
protected final void assertNotDisposed()
{
if( disposed() )
{
final String msg = propertyAlreadyDisposed.format( this.definition.name() );
throw new IllegalStateException( msg );
}
}
protected final IllegalArgumentException createIllegalPathException( final ModelPath path )
{
final String message = illegalPathException.format
(
element().type().getModelElementClass().getName(),
name(),
path.toString()
);
return new IllegalArgumentException( message );
}
private final class SuspendFilter implements Filter<EventDeliveryJob>
{
@Override
public boolean allows( final EventDeliveryJob job )
{
if( ! ( job.listener() instanceof NonSuspendableListener ) )
{
final Event event = job.event();
if( event instanceof PropertyEvent )
{
return ! ( Property.this.holds( ( (PropertyEvent) event ).property() ) );
}
else if( event instanceof ElementEvent )
{
return ! ( Property.this.holds( ( (ElementEvent) event ).element() ) );
}
}
return true;
}
}
}