JavaTyped and Targeted Property Change Events in Java

Typed and Targeted Property Change Events in Java

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Listening for bound property changes of a JavaBean is simple enough, and
determining which bean fired the event is as straightforward as calling
PropertyChangeEvent.getSource(). But what to do if the JavaBean in
question has complex property values, each of which can have its own bound
properties? This article examines a method for representing targeted
events—those that may represent changes in objects other than the source. It
also introduces an adapter that allows type-aware property change events using
Java 5.0 generics. Examples are provided using the Guise™ Internet application
framework.


Typed Property Change Events


When Sun added the JavaBeans component architecture, Java acquired a standard way to
conceptualize properties of objects that were richer than mere data
fields. Besides the ability to perform custom actions when properties are read
and/or written, using so-called getter and setter methods,
respectively, Java allowed third-party observer objects to listen for property
changes and perform their own actions in response. Such properties which notify
listeners of changes are referred to as bound properties. The
foundational classes of this architecture are found in the java.beans
package
.


The two fundamental classes that implement bound properties in JavaBeans are
PropertyChangeListener and PropertyChangeEvent. Sun
provided the PropertyChangeSupport class to assist developers in
creating classes with bound properties, obviating the need for a class to keep
track of its own property change listeners and to manually fire property change
events to those listeners. Using these classes, a simple Automobile
class could implement an engine bound property, assuming that an
Engine class exists. (Note that we require that an engine always be
non-null to simplify the logic. Without this requirement, the code
would need extra checks for null throughout.)

import java.beans.*;

public class Automobile
{
protected final PropertyChangeSupport propertyChangeSupport;
private Engine engine;

/**Default constructor with a default engine.*/
public Automobile()
{
propertyChangeSupport=new PropertyChangeSupport(this);
engine=new Engine();
}

/**@return The car’s current engine.*/
public Engine getEngine() {return engine;}

/**Sets the car’s engine.
@param newEngine The new engine.
@exception NullPointerException if the engine is
<code>null</code>.
*/
public void setEngine(final Engine newEngine)
{
if(newEngine==null)
{
throw new NullPointerException(“No engine provided.”);
}
if(!engine.equals(newEngine))
{
final Engine oldEngine= engine;
engine=newEngine;
propertyChangeSupport.firePropertyChange(“engine”,
oldEngine,
newEngine);
}
}

/**Add a property change listener for a specific property.
@param propertyName The name of the property to listen on.
@param listener The <code>PropertyChangeListener</code>
to be added.
*/
public void addPropertyChangeListener(final String propertyName,
final PropertyChangeListener listener)
{
propertyChangeSupport.addPropertyChangeListener(propertyName,
listener);
}

/**Remove a property change listener for a specific property.
@param propertyName The name of the property that was listened on.
@param listener The <code>PropertyChangeListener</code>
to be removed
*/
public void removePropertyChangeListener(final String propertyName,
final PropertyChangeListener listener)
{
propertyChangeSupport.removePropertyChangeListener(propertyName,
listener);
}
}


An interested class could listen for the car’s engine changing by adding a
property change listener, usually an anonymous inner class:

final Automobile automobile=new Automobile();
automobile.addPropertyChangeListener(“engine”,
new PropertyChangeEvent()
{
public void propertyChange(
final PropertyChangeEvent propertyChangeEvent)
{
final Engine newEngine=
(Engine)propertyChangeEvent.getNewValue();
if(newEngine.getSize()>8)
{
Systen.out.println(“That’s a big engine.”);
}
}
});

That’s easy enough. But the old and new values of
PropertyChangeEvent are always expressed as Object,
forcing us to cast those values, even when we know ahead of time what type to
expect. When we listen for the “engine” property, we expect the
value to be an Engine. Wouldn’t it be nice if
PropertyChangeEvent used generics, so that its values were
automatically returned as the expected types?


Unfortunately, PropertyChangeEvent was created long before
generics were added to the Java language in version 5.0, but using another Java
feature, covariance, it’s possible to create an adapter class that
provides us with typed values while providing backwards-compatibility with
PropertyChangeEvent. This part of the task is surprisingly easy;
here is the basic structure of a class,
com.garretwilson.beans.GenericPropertyChangeEvent<V>, that
does just that:

/**A property value change event is a Java Beans property change
event retrofitted to use generics to cast to proper value type.
@param <V> The type of property value.
@author Garret Wilson
*/
public class GenericPropertyChangeEvent<V>
extends PropertyChangeEvent
{

public GenericPropertyChangeEvent(final Object source,
final String propertyName, final V oldValue, V newValue)
{
super(source, propertyName, oldValue, newValue);
}

@SuppressWarnings(“unchecked”)
public GenericPropertyChangeEvent(final PropertyChangeEvent
propertyChangeEvent)
{
this(propertyChangeEvent.getSource(),
propertyChangeEvent.getPropertyName(),
(V)propertyChangeEvent.getOldValue(),
(V)propertyChangeEvent.getNewValue());
setPropagationId(propertyChangeEvent.getPropagationId());
}

@SuppressWarnings(“unchecked”)
public V getOldValue()
{
return (V)super.getOldValue();
}

@SuppressWarnings(“unchecked”)
public V getNewValue()
{
return (V)super.getNewValue();
}
}


So far the main functionality of the class is to cast the old and new values
to the generic type, V, before returning them. Because Java doesn’t
keep track of generics at runtime, the real return types of the
getOldValue() and getNewValue() methods after erasure
are Object, anyway. The cast to the generic type isn’t actually
performed here at runtime, so the @SuppressWarnings(“unchecked”)
annotation is needed to prevent the compiler from alerting us to this fact.
Whatever code uses these methods will perform the appropriate cast, however,
providing equivalence to the property change listener code we had earlier,
except without the need to code casts by hand. We’ve even provided a copy
constructor to allow creating generic property change events from standard
non-generic property change events.


There’s a problem, however: the Automobile class fires a normal
PropertyChangeEvent, not a
GenericPropertyChangeEvent<V>. We could change the
Automobile class to throw a
GenericPropertyChangeEvent<V>, which is compatible with
PropertyChangeEvent, but then we’d have to cast the event inside
our PropertyChangeListener, which defeats the purpose of this
exercise. It looks like we’ll have to create a custom
GenericPropertyChangeListener<V> as well:

/**A Java Beans property change listener retrofitted
to use generics to cast to proper value type.
@param <V> The type of property value.
@author Garret Wilson
*/
public interface GenericPropertyChangeListener<V>
extends PropertyChangeListener
{
public void propertyChange(final GenericPropertyChangeEvent<V>
genericPropertyChangeEvent);
}

Well, that was certainly easy enough. But more problems arise. First, the
java.beans.PropertyChangeSupport class keeps track of
PropertyChangeListeners, not
GenericPropertyChangeListeners, and therefore will call the
PropertyChangeListener.propertyChange() method rather than the
generic version. We could scrap PropertyChangeSupport and keep
track of the listeners ourselves, but there bigger problem: our
Automobile class would no longer be compatible with the standard
Java bound property contract, which requires registration of
PropertyChangeListeners and firing of property change events to the
non-generics-aware version of propertyChange(). Third party tools
would not be able to work with our JavaBean.


The trick is to use a special base listener class that is compatible with
both PropertyChangeListener and
GenericPropertyChangeListener<V> and that converts the
PropertyChangeEvent to a
GenericPropertyChangeEvent<V> as needed. That class,
com.garretwilson.beans.AbstractGenericPropertyChangeListener, is
presented below:

/**A Java Beans property change listener
retrofitted to use generics to cast to proper value type.
@param <V> The type of property value.
@author Garret Wilson
*/
public abstract class AbstractGenericPropertyChangeListener<V>
implements GenericPropertyChangeListener<V>
{

/**Called when a bound property is changed.
This non-generics version calls the generic version,
creating a new event if necessary.
No checks are made at compile time to ensure the given event
actually supports the given generic type.
@param propertyChangeEvent An event object describing
the event source, the property that has changed,
and its old and new values.
@see GenericPropertyChangeListener#propertyChange
(GenericPropertyChangeEvent)
*/
@SuppressWarnings(“unchecked”)
public final void propertyChange(final PropertyChangeEvent
propertyChangeEvent)
{
propertyChange((GenericPropertyChangeEvent<V>)
getGenericPropertyChangeEvent(propertyChangeEvent));
}

/**Converts a property change event to a generics-aware
property value change event.
@param propertyChangeEvent An event object describing
the event source, the property that has changed,
and its old and new values.
@return A generics-aware property change event,
either cast from the provided object
or created from the provided object’s values as appropriate.
*/
@SuppressWarnings(“unchecked”)
public static <T> GenericPropertyChangeEvent<T>
getGenericPropertyChangeEvent(final PropertyChangeEvent
propertyChangeEvent)
{
if(propertyChangeEvent instanceof GenericPropertyChangeEvent)
{
return (GenericPropertyChangeEvent<T>)propertyChangeEvent;
}
else //if the event is a normal property change event
{
return new GenericPropertyChangeEvent<T>(
propertyChangeEvent); //create a copy of the event
}
}
}


Fulfilling the contract of PropertyChangeListener, this class
overrides propertyChange(PropertyChangeEvent), but then converts
the event to a GenericPropertyChangeEvent<T> and passes it to
the generic method version,
propertyChange(GenericPropertyChangeEvent<T>). The conversion
in the utility method <T> GenericPropertyChangeEvent<T>
getGenericPropertyChangeEvent(final PropertyChangeEvent
propertyChangeEvent)
is efficient: if the event is already a
GenericPropertyChangeEvent<T>, there’s no need to create a
new event. Otherwise, the method creates a new
GenericPropertyChangeEvent<T> from the plain
PropertyChangeEvent using the copy constructor we created
above.


To use this efficiency, we’ll need to modify the Automobile
class to fire GenericPropertyChangeEvents. (The class would work
just fine without this, but would require the unnecessary creation of new
objects from generics-aware listeners.) It turns out that
PropertyChangeSupport.firePropertyChange(String, Object, Object)
just creates a PropertyChangeEvent and sends that to
PropertyChangeSupport.firePropertyChange(PropertyChangeEvent). The
Automobile class can create its own
GenericPropertyChangeEvent<Engine> and fire that object when
needed:

/**Sets the car’s engine.
@param newEngine The new engine.
@exception NullPointerException if the engine
is <code>null</code>.
*/
public void setEngine(final Engine newEngine)
{
if(newEngine==null)
{
throw new NullPointerException(“No engine provided.”);
}
if(!engine.equals(newEngine))
{
final Engine oldEngine= engine;
engine=newEngine;
final PropertyChangeEvent propertyChangeEvent=
new GenericPropertyChangeEvent<Engine>(this,
“engine”,
oldEngine,
newEngine);
propertyChangeSupport.firePropertyChange(propertyChangeEvent);
}
}

All the pieces are now in place. We can now listen to the car’s engine
changing using our generics-aware classes with no casts:

final Automobile automobile=new Automobile();
automobile.addPropertyChangeListener(“engine”,
new AbstractGenericPropertyChangeEvent<Engine>()
{
public void propertyChange(
final GenericPropertyChangeEvent<Engine>
propertyChangeEvent)
{
final Engine newEngine=propertyChangeEvent.getNewValue();
if(newEngine.getSize()>8)
{
Systen.out.println(“That’s a big engine.”);
}
}
});

Furthermore, non-generics-aware PropertyChangeListener instances
can still register with an Automobile instance, and they will
function as normal. Even better, we can use the
AbstractGenericPropertyChangeEvent<V> registration pattern to
register with any JavaBean, whether or not it is generics-aware. Our code will
perform casts implicitly, obviating the need for us to perform this tedious and
error-prone process ourselves. This pattern additionally imposes consistency on
our code—if we copy part of this routine to another property change listener
that expects a type other than Engine, our code won’t compile,
whereas an erroneous hand-coded cast to Engine in a
non-generics-aware property change listener would have gone unnoticed until
runtime.

Targeted Property Change Events


Our current scheme works well for changing automobile engines, and for
objects with simple properties the standard JavaBeans bound property paradigm
gets the job done. But for complex properties—those properties the values of
which have their own properties that can change—this paradigm has it
limitations. What if some outside object want to listen for changes in the
engine size as well, for example?


One solution would be for the external object to register a property change
listener directly with the Automobile‘s engine property value. This
quickly becomes cumbersome, because the value of the engine
property can change. The external class must therefore also register a property
change listener with the Automobile instance, listening for the
“engine” property to change, and then unregister its event listener
from the old engine and register it with the new one. Remembering to do this
(not to mention documenting that this must be done) becomes unmanageable.
Wouldn’t it be better if we could do this once in the Automobile
class, and propagate any property changes in the Engine to the
property change listeners of Automobile?


This is a valid approach, but it leaves one detail unresolved: How does a
PropertyChangeListener determine whether a property change was
performed on the Automobile or on its contained
Engine? We could have the Automobile forward the
Engine property change events unchanged, so that
PropertyChangeEvent.getSource() would equal the Engine
instance as appropriate, but if one property change listener is listening to
several Automobiles it prevents them from using
PropertyChangeEvent.getSource() to find which
Automobile‘s Engine changed properties. Besides, the
contract of PropertyChangeEvent.getSource() (as stated by the API
of java.util.EventObject) is that “All Events are constructed with
a reference to the object, the “source”, that is logically deemed to be the
object upon which the Event in question initially occurred upon.” While this is
slightly ambiguous in our case as to whether “initially occurred upon” refers to
the object that changed its properties or the object that fired the event, it
seems pragmatically useful for a PropertyChangeListener to expect
PropertyChangeEvent.getSource() to refer to the object with which
the listener registered. This would favor the “object that fired the event as
source” interpretation.


So there needs to be a separate method of PropertyChangeEvent
that indicates the object the property of which changed, regardless of which
object is firing the event. This value would stay the same regardless of how
many times the event was propagated. Indeed it is possible the authors of Java
had something similar in mind for the future, for the API of
java.beans.PropertyChangeEvent.getPropagationId() states:



The “propagationId” field is reserved for future use. In Beans 1.0 the sole
requirement is that if a listener catches a PropertyChangeEvent and then fires
a PropertyChangeEvent of its own, then it should make sure that it propagates
the propagationId field from its incoming event to its outgoing
event.


That sounds a lot like the functionality we want, but with no more
information on semantics, and with no conventions relating to this field in
widespread use, it seems foolhardy to hijack this field for own purposes. If
future versions of Java were to use the propagationId field for some other
purpose, it could cause serious backwards-compatibility issues with our code. It
again seems that the safest route is to add in our own extensions to the
JavaBeans component architecture.


We can start by creating an interface that can be mixed in with any event and
that indicates the target of the event—the object to which the action was
directed that caused the event, whether or not that object actually fired the
event object in question. This is done in
com.garretwilson.event.TargetedEvent, which could be used with any
type of event, such as a property change event or an action event:

/**An interface for an event that knows its target,
or the object to which the event applies
(which may be different than the object that fired the event).
@author Garret Wilson
*/
public interface TargetedEvent
{

/**Returns the object to which the event applies.
This may be a different than <dfn>source</dfn>,
which is the object that generated this event instance.
@return The target of the event, or <code>null</code>
if the event target is not known.
*/
public Object getTarget();
}


We can now make GenericPropertyChangeEvent<V>, which we
created earlier, compatible with our new targeted event framework. We’ll add new
copy constructors that will allow new events to be created with different
sources while still maintaining the same target. We’ll also upgrade the old
source-only constructors to automatically set the target to the same value as
the source, for those instances in which there is no need to maintain a separate
target and the class is being used traditionally:

/**A property value change event is a Java Beans
property change event retrofitted to use generics
to cast to proper value type.
This event is also <dfn>targeted</dfn>,
specifying an event target which may or may not
be the same as the source object firing this event.
@param <V> The type of property value.
@author Garret Wilson
*/
public class GenericPropertyChangeEvent<V>
extends PropertyChangeEvent implements TargetedEvent
{

/**The target of the event, or <code>null</code>
if the event target is not known.*/
private final Object target;

/**Returns the object to which the event applies.
This may be a different than <dfn>source</dfn>,
which is the object that generated this event instance.
@return The target of the event.
*/
public Object getTarget() {return target;}

/**Source and property name constructor with old and new values.
The target will be set to be the same as the given source.
@param source The bean that fired the event.
@param propertyName The programmatic name of the property
that was changed.
@param oldValue The old value of the property,
or <code>null</code> if no old value is not available.
@param newValue The new value of the property,
or <code>null</code> if the new value is not available.
*/
public GenericPropertyChangeEvent(final Object source,
final String propertyName, final V oldValue, V newValue)
{
this(source, source, propertyName, oldValue, newValue);
}

/**Source, target, and property name constructor
with old and new values.
@param source The bean that fired the event.
@param target The target of the event.
@param propertyName The programmatic name of the property
that was changed.
@param oldValue The old value of the property,
or <code>null</code> if no old value is not available.
@param newValue The new value of the property,
or <code>null</code> if the new value is not available.
*/
public GenericPropertyChangeEvent(final Object source,
final Object target, final String propertyName,
final V oldValue, V newValue)
{
super(source, propertyName, oldValue, newValue);
this.target= target;
}

/**Property change event copy constructor.
@param propertyChangeEvent A property change event the values
of which will later cast to this class’ generic type.
*/
public GenericPropertyChangeEvent(final PropertyChangeEvent
propertyChangeEvent)
{
this(propertyChangeEvent.getSource(), propertyChangeEvent);
}

/**Property change event copy constructor
that specifies a different source.
If the property change event is a {@link TargetedEvent},
the target is copied from that event;
otherwise, the given source will be used as the target.
@param source The object on which the event initially occurred.
@param propertyChangeEvent A property change event the values
of which will later cast to this class’ generic type.
*/
@SuppressWarnings(“unchecked”)
public GenericPropertyChangeEvent(final Object source,
final PropertyChangeEvent propertyChangeEvent)
{
this(source, propertyChangeEvent instanceof TargetedEvent
? ((TargetedEvent)propertyChangeEvent).getTarget() : source,
propertyChangeEvent.getPropertyName(),
(V)propertyChangeEvent.getOldValue(),
(V)propertyChangeEvent.getNewValue());
setPropagationId(propertyChangeEvent.getPropagationId());
}

/**@return The old value of the property,
or <code>null</code> if the old value is not available.*/
@SuppressWarnings(“unchecked”)
public V getOldValue()
{
return (V)super.getOldValue();
}

/**@return The new value of the property,
or <code>null</code> if the new value is not available.*/
@SuppressWarnings(“unchecked”)
public V getNewValue()
{
return (V)super.getNewValue();
}
}


The Automobile class can now repeat any property changes of the
contained Engine object by creating a new event object with a new
source, but keeping the same target. This is best done using the
GenericPropertyChangeEvent<V> copy constructor above that
takes a different source as one of its arguments. The new
Automobile class is shown below:

public class Automobile
{
protected final PropertyChangeSupport propertyChangeSupport;
protected final PropertyChangeListener repeatPropertyChangeListener=
new PropertyChangeListener()
{
public void propertyChange(final PropertyChangeEvent
propertyChangeEvent)
{
final PropertyChangeEvent repeatPropertyChangeEvent=
new GenericPropertyChangeEvent<Object>(
Automobile.this, propertyChangeEvent);
propertyChangeSupport.firePropertyChange(
repeatPropertyChangeEvent);
}
};
private Engine engine;

/**Default constructor with a default engine.*/
public Automobile()
{
propertyChangeSupport=new PropertyChangeSupport(this);
engine=new Engine();
engine.addPropertyChangeListener(repeatPropertyChangeListener);
addPropertyChangeListener(“engine”,
new AbstractGenericPropertyChangeEvent<Engine>()
{
public void propertyChange(
final GenericPropertyChangeEvent<Engine>
propertyChangeEvent)
{
propertyChangeEvent.getOldValue().
removePropertyChangeListener(repeatPropertyChangeListener);
propertyChangeEvent.getNewValue().
addPropertyChangeListener(repeatPropertyChangeListener);
}
});
}

/**@return The car’s current engine.*/
public Engine getEngine() {return engine;}

/**Sets the car’s engine.
@param newEngine The new engine.
@exception NullPointerException if the engine
is <code>null</code>.
*/
public void setEngine(final Engine newEngine)
{
if(newEngine==null)
{
throw new NullPointerException(“No engine provided.”);
}
if(!engine.equals(newEngine))
{
final Engine oldEngine= engine;
engine=newEngine;
final PropertyChangeEvent propertyChangeEvent=
new GenericPropertyChangeEvent<Engine>(this,
“engine”,
oldEngine,
newEngine);
propertyChangeSupport.firePropertyChange(propertyChangeEvent);
}
}

/**Add a property change listener for a specific property.
@param propertyName The name of the property to listen on.
@param listener The <code>PropertyChangeListener</code>
to be added.
*/
public void addPropertyChangeListener(final String propertyName,
final PropertyChangeListener listener)
{
propertyChangeSupport.addPropertyChangeListener(propertyName,
listener);
}

/**Remove a property change listener for a specific property.
@param propertyName The name of the property that was listened on.
@param listener The <code>PropertyChangeListener</code>
to be removed
*/
public void removePropertyChangeListener(final String propertyName,
final PropertyChangeListener listener)
{
propertyChangeSupport.removePropertyChangeListener(propertyName,
listener);
}
}


There are three important parts to this property change repeater pattern. The
first is the repeater PropertyChangeListener, which can be
registered with any class. It takes whatever property change that has occurred
and refires the event after creating a new event with an updated source of the
object firing the event. (It is important to note that
Automobile.this is used to represent the Automobile
instance as the source rather than the anonymous inner class.) Secondly, the
repeater is registered with the Engine instance as soon as it is
created. Thirdly, because the engine property can change, a
separate listener is registered with the Automobile instance itself
for notification of when the engine property value changes. When
this happens, the repeater is unregistered from the old Engine and
then registered with the new Engine.


Now an external class can be notified of property changes of the
Engine property of the Automobile by registering with
the Automobile either for all property change events or only for
specific Engine-specific properties. If there is ever a question of
whether a property of Automobile or Engine has
changed, this may be resolved by examining
TargetedEvent.getTarget() if the PropertyChangeEvent
in question implements TargetedEvent.


The concept of a targeted event is especially useful when an object can have
an arbitrary number of contained objects over an arbitrary number of
hierarchical levels. As an example, consider the TreeControl
component in the com.guiseframework.component package of the Guise™ Internet application
framework. A TreeControl contains a root
TreeNodeModel<V> which itself can contain an arbitrary number
of TreeNodeModel<V> children, each of which in turn can
contain other tree nodes, and so on. A common use case is to listen for the
selection by the user of one of the nodes in a tree.


An application simply cannot practically register and unregister a property
change listeners with each TreeNodeModel<V> as it is added or
removed from the tree. The Guise™ TreeControl therefore uses the
typed, targeted property change architecture described above to propagate
property changes of each TreeNodeModel<V> up the tree
hierarchy until it is finally repeated from the TreeControl. Any
property change event fired from the control will properly indicate the
TreeControl as its source, and indicate the tree node
with the changed property as its target. An application can
therefore listen directly to the TreeControl to be notified when a
tree node is selected, for example:

final TreeControl treeControl=new TreeControl();
treeControl.addPropertyChangeListener(
TreeNodeModel.SELECTED_PROPERTY,
new AbstractGenericPropertyChangeListener<Boolean>()
{
public void propertyChange(
final GenericPropertyChangeEvent<Boolean>
propertyChangeEvent)
{
if(propertyChangeEvent.getTarget() instanceof TreeNodeModel)
{
final TreeNodeModel<?> treeNodeModel=
(TreeNodeModel<?>)propertyChangeEvent.getTarget();
if(Boolean.TRUE.equals(propertyChangeEvent.getNewValue()))
{
//the tree node was just selected
}
}
}
});

The application listens for the TreeNodeModel.SELECTED_PROPERTY
changing by registering one property change listener with the
TreeControl instance. Once the property changes, the listener
verifies that a tree node is indeed the object with the changed selection
status. If the new selected status is true, the application may
decide to perform some special action resulting from the selection change,
perhaps based upon the specific tree node target as reported in
propertyChangeEvent.getTarget().


Summary


This JavaBean extension architecture of typed, targeted property change
events not only is simple and powerful, it also is interoperable with the
existing JavaBeans component architecture. Through the use of generics it
obviates the need for tedious manual casting of values while imposing
consistency and internal type checking within a property change listener. By
introducing the concept of an event target, the framework allows facile
propagation of property change events to a single listening point without losing
the identification of the original object the property of which was changed. A
developer can experiment with the examples presented here using the free
development version of the Guise™ framework library, available at http://www.guiseframework.com/.


About the Author


Garret Wilson provides
consulting on Internet standards through his company, GlobalMentor, Inc. He is
currently perfecting the AJAX-enabled Guise™ Internet application framework.


Copyright © 2006 Garret Wilson.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories