PROPOSAL: Lightweight Properties
Reinier Zwitserloot
reinier at zwitserloot.com
Tue Mar 3 15:11:41 PST 2009
But it seems that we _would_ lose a thing or two with this proposal.
For example, if a complete properties proposal supports full backwards
compatibility and generates getters and setters, then we have a bunch
of code around that uses the foo#bar notation, which is by then
already outdated, and would in fact get in the way of using foo#bar
notation as a shorthand for calling the appropriate getter/setter. I
don't know if that is a desired syntax sugar, but the point is: Your
proposal would effectively make it impossible to add that later, as it
would already be shorthand for creating a Property object.
The # and the backtick are the only easily typed symbols that have no
meaning whatsoever in java right now. Using one up for an simplified
properties proposal is another serious disdvantage. I don't think your
proposal makes property support light enough to warrant its inclusion
in project coin. That's not to say I have something against
properties; on the contrary, I love the idea and I'm very impressed
with the way properties work in JavaFX. All the more reason to get it
right instead of using a bandage.
--Reinier Zwitserloot
On Mar 3, 2009, at 23:00, David Goodenough wrote:
> Well that depends on what you mean by a complete proposal.
>
> There are two parts to the use of Properties. There is the framework
> side, inside BeansBinding or JPA and then there is the consumer
> side, the application code.
>
> My proposal is very simple on the consumer side (the only bit needed
> is the # notation). You can use clasical getters and setters (a
> nuisance
> but everyone understands them), or the simple getter/setter mechanism
> that my Property provides. So for the consumer my proposal is real
> simple. All you need do is add (either explicity or by byte code
> enhancement)
> a PropertyChangeSupport object and the relevant methods and you
> are home and dry.
>
> For the Frameworks, well you only write them once. Even so something
> nice and simple appeals and my proposal is simple.
>
> It may not be Beans as we know it, but they never were integrated
> properly into Java. But its really not that dissimilar just slightly
> different.
>
> So other than a little sytactic sugar (not having to code the
> PropertyChangeSupport object) we have not really lost anything of
> the "full" solution. Having a Bound annotation which would add this
> under the
> covers with a byte code enhancer would not be difficult. The implicit
> getters and setters come with the package. So the question to be
> asked whether a fuller solution is really needed.
>
> David
>
> On Tuesday 03 March 2009, Reinier Zwitserloot wrote:
>> The language change required for your proposal is indeed lighter, but
>> to understand it, it is seems a lot more complicated.
>>
>> Also, it would be infeasible to introduce a 'lightweight' (according
>> to anyone's definition) proposal now that is lacking in certain
>> aspects, and then fix it later, unless that fix is syntactically very
>> similar and backwards- and migration compatible. That's the major
>> beef
>> I have with this proposal: It effectively shuts the door on any other
>> property proposal. In my view, a proposal solves the problem
>> properly,
>> or shouldn't be added at all; no quick hacky fixes that aren't part
>> of
>> a planned evolution path to a complete solution.
>>
>> If you can highlight how a complete properties proposal will
>> seamlessly work with your syntax, I'm more inclined to like it.
>>
>> --Reinier Zwitserloot
>>
>> On Mar 3, 2009, at 22:04, David Goodenough wrote:
>>> Yes I do. What you propose is much more invasive. Look at it
>>> again and maybe you will see the Light(ness).
>>>
>>> David
>>>
>>> On Tuesday 03 March 2009, Reinier Zwitserloot wrote:
>>>> You call that lightweight?
>>>>
>>>> How about following the beans spec more to the letter and just
>>>> generate the addPropertyChangeListener,
>>>> removePropertyChangeListener,
>>>> setX(), and get/isX() method in response to seeing a keyword or
>>>> annotation on a given field. You'll have to work out the details,
>>>> but
>>>> that sounds far, far simpler to understand.
>>>>
>>>> You'll need to flesh this out, but it would look something like:
>>>>
>>>> public class Foo {
>>>> private property int x;
>>>> }
>>>>
>>>> Which would generate the addPropertyChangeListener,
>>>> removePropertyChangeListener, setX, getX methods, all public, along
>>>> with the required infrastructure to make it tick. If you don't like
>>>> the generation, for example because you want the setter to be
>>>> package
>>>> private, you just add the setter in the source file; the keyword
>>>> will
>>>> only generate the missing stuff. It doesn't cover every use case,
>>>> but
>>>> there's always the alternative of doing whatever people do now with
>>>> beans. Something you didn't mention in your proposal, by the way.
>>>>
>>>> I think there's also a fully fleshed out property proposal
>>>> (including
>>>> a 'property' keyword) out there somewhere.
>>>>
>>>> Possibly make a way to opt out of generating the property change
>>>> listener support, and just the getters/setters.
>>>> --Reinier Zwitserloot
>>>>
>>>> On Mar 3, 2009, at 15:59, David Goodenough wrote:
>>>>> Below is my proposal for Lightweight Properties. I know that the
>>>>> syntax
>>>>> change is an abbomination to some people, but I have tried to
>>>>> reduce
>>>>> this to its absolute minimum, while still getting a significant
>>>>> benefit.
>>>>>
>>>>> PROJECT COIN SMALL LANGUAGE CHANGE PROPOSAL FORM v1.0
>>>>>
>>>>> AUTHOR(S):
>>>>>
>>>>> David Goodenough, long time Java user. I can be reached at
>>>>> david.goodenough at linkchoose.co.uk.
>>>>>
>>>>> OVERVIEW
>>>>>
>>>>> FEATURE SUMMARY:
>>>>>
>>>>> Lightweight Property support
>>>>>
>>>>> MAJOR ADVANTAGE:
>>>>>
>>>>> Both BeansBinding (whether JSR-295 or others such an JFace or the
>>>>> JGoodies
>>>>> binding) and the JPA Criteria API currently require field names
>>>>> (as
>>>>> Strings)
>>>>> as arguments, which an IDE/compiler can not check. With this
>>>>> proposal the
>>>>> strings would be abandoned, and the IDE/compiler will be able to
>>>>> check the
>>>>> correctness of the code.
>>>>>
>>>>> MAJOR BENEFIT:
>>>>>
>>>>> Manual checking no longer required. This proposal introduces a
>>>>> simple well
>>>>> defined IDE/compiler checkable solution.
>>>>>
>>>>> MAJOR DISADVANTAGE:
>>>>>
>>>>> It is a language change, and this seems to upset some people.
>>>>>
>>>>> ALTERNATIVES:
>>>>>
>>>>> None really, apart from using another language or continuing to
>>>>> use
>>>>> String
>>>>> names. The existing solutions all require String names which are
>>>>> uncheckable.
>>>>>
>>>>> EXAMPLES
>>>>>
>>>>> Lets assume we have a POJO called foo, of type Foo with a field
>>>>> bar
>>>>> of type
>>>>> Bar, which itself has a field of type Jim called jim.
>>>>>
>>>>> There are two forms of lightweight properties:-
>>>>>
>>>>> 1) foo#bar would be translated by the compiler into:-
>>>>>
>>>>> new Property<Foo,Bar>(foo,"bar");
>>>>>
>>>>> while foo#bar#jim would be translated into:-
>>>>>
>>>>> new Property<Foo,Jim>(foo,"bar","jim");
>>>>>
>>>>> 2) Foo#bar would be translated into:-
>>>>>
>>>>> new Property<Foo,Bar>(Foo.class,"bar");
>>>>>
>>>>> while Foo#bar#jim would be translated into:-
>>>>>
>>>>> new Property<Foo,Jim>(Foo.class,"bar","jim");
>>>>>
>>>>> These two forms create (1) a bound Property, or (2) an unbound
>>>>> one.
>>>>> Bound
>>>>> Properties are explicitly bound to a particular instance of a
>>>>> class
>>>>> (in this
>>>>> case foo), while unbound Properties are templates which can be
>>>>> applied to any
>>>>> instance of class Foo. Actually bound properties can also be
>>>>> used as
>>>>> unbound
>>>>> properties, but that is a harmless and useful side effect not a
>>>>> primary
>>>>> intent.
>>>>>
>>>>> The Property class would need to be added (it is appended below),
>>>>> and should
>>>>> be added either to the java.beans package or to the
>>>>> java.lang.reflect package
>>>>> (with which is probably has more in common).
>>>>>
>>>>> Syntactically a "#" can be placed wherever a "." can be placed
>>>>> (except inside
>>>>> a number), and the same checks need to be made (that each field to
>>>>> the right
>>>>> of a # is a field in the left hand side) as would be made for a
>>>>> ".".
>>>>> The only
>>>>> difference is in field visibility - For the "#" any field is
>>>>> visible, which
>>>>> follows the model currently available in the Field class with
>>>>> getDeclaredFields(). It also follows the model that while a field
>>>>> might be
>>>>> private and therefore not directly accessible from outside,
>>>>> getters
>>>>> and
>>>>> setters can provide access.
>>>>>
>>>>> The Property object provides type safe access to the field in the
>>>>> form of
>>>>> getters and setters. These come in pairs, one for bound and the
>>>>> other for
>>>>> unbound access. So for bound access no object is required to fetch
>>>>> the value,
>>>>> for an unbound object the parent object needs to be specified.
>>>>> So if
>>>>> we
>>>>> have:-
>>>>>
>>>>> Property<Foo,Bar>prop = foo#bar;
>>>>>
>>>>> we can later say:-
>>>>>
>>>>> Bar b = prop.get();
>>>>>
>>>>> or for an unbound one from a second Foo object foo2:-
>>>>>
>>>>> Bar b = prop.get(foo2);
>>>>>
>>>>> The getters and setters in the Property object will defer to
>>>>> explicitly coded
>>>>> getters and setters if present, otherwise they will use the Field
>>>>> getter and
>>>>> setter.
>>>>>
>>>>> If a setter is not explicitly coded, the implicit setter will look
>>>>> for a
>>>>> PropertyChangeSupport object in the parent object of the rightmost
>>>>> field and
>>>>> fire a PropertyChangeEvent to that object.
>>>>>
>>>>> There are also two Annotations provided by the Property class,
>>>>> ReadOnly and
>>>>> WriteOnly. These stop implicit getters and setters from trying to
>>>>> read/write
>>>>> the property.
>>>>>
>>>>> Talking of Annotations, this notation can also be used to get at
>>>>> the
>>>>> Annotations for a field. So to test for the presence of an
>>>>> Annotation Ann on
>>>>> Foo.bar we would use:-
>>>>>
>>>>> if(Foo#bar.getFields()[0].isAnnotationPresent(Ann.class)) ...
>>>>>
>>>>> SIMPLE EXAMPLE:
>>>>>
>>>>> To take an example from BeansBinding (taken from Shannon Hickey's
>>>>> blog):-
>>>>>
>>>>> // create a BeanProperty representing a bean's firstName
>>>>> Property firstP = BeanProperty.create("firstName");
>>>>> // Bind Duke's first name to the text property of a Swing
>>>>> JTextField
>>>>> BeanProperty textP = BeanProperty.create("text");
>>>>> Binding binding = Bindings.createAutoBinding(READ_WRITE, duke,
>>>>> firstP, textfield, textP);
>>>>> binding.bind();
>>>>>
>>>>>
>>>>> would instead be written:-
>>>>>
>>>>> Binding binding = Bindings.createAutoBinding(READ_WRITE,
>>>>> duke#firstName, textfield#text);
>>>>> binding.bind();
>>>>>
>>>>> which of course can be checked by the IDE/compiler, and will not
>>>>> wait until
>>>>> run time (not even instantiation time) to show up the error.
>>>>>
>>>>> ADVANCED EXAMPLE:
>>>>>
>>>>> For a JComboBox (or JList or JTable or JTree) there is a need to
>>>>> map
>>>>> a list of
>>>>> objects to the value strings (or column contents). For this we
>>>>> need
>>>>> to have
>>>>> an unbound Property which can be applied to each element of the
>>>>> list.
>>>>>
>>>>> Duke duke;
>>>>> List<Duke>dukes;
>>>>> BoundComboBox combo = new
>>>>> BoundComboBox(dukes,Duke#fullname,this#duke);
>>>>>
>>>>> and now the combo box will be populated from the list dukes, and
>>>>> the
>>>>> display
>>>>> values in the list will be taken from the fullname field of each
>>>>> Duke
>>>>> element, and the initial value will be set from the local class
>>>>> field duke
>>>>> and any changes to the combo box selected element will be
>>>>> reflected
>>>>> back to
>>>>> the duke field.
>>>>>
>>>>> DETAILS
>>>>>
>>>>> SPECIFICATION:
>>>>>
>>>>> This proposal adds a new syntactic element, "#", which can be used
>>>>> in the same
>>>>> way that "." can be used to qualify fields within a Java object.
>>>>>
>>>>> COMPILATION:
>>>>>
>>>>> This proposal requires no change to the class files, and is
>>>>> implemented by a
>>>>> simple generation of the required instance using the relevant
>>>>> Property
>>>>> constructor. Obviously the compiler would have to make sure that
>>>>> the
>>>>> use that
>>>>> the property object was being put to (in the examples above the
>>>>> left
>>>>> hand
>>>>> side of the assignment) had the correct Generic attributes.
>>>>>
>>>>> TESTING:
>>>>>
>>>>> How can the feature be tested?
>>>>>
>>>>> LIBRARY SUPPORT:
>>>>>
>>>>> The new Property class is required (see below).
>>>>>
>>>>> REFLECTIVE APIS:
>>>>>
>>>>> No changes are required to the reflective APIs although it makes
>>>>> extensive use
>>>>> of those APIs.
>>>>>
>>>>> OTHER CHANGES:
>>>>>
>>>>> No other changes are requires.
>>>>>
>>>>> MIGRATION:
>>>>>
>>>>> Fortunately there is no code that is formally part of J2SE 6 which
>>>>> uses such
>>>>> Properties. There are however two proposals which will need it
>>>>> (BeansBinding
>>>>> and JPA Criteria API), but neither of these seem to be destined to
>>>>> be part of
>>>>> J2SE 7 (BeansBinding seems to have died the death and the Criteria
>>>>> API would
>>>>> be part of the next J2EE which will follow J2SE 7), so this will
>>>>> provide a
>>>>> base for them to use and no existing code need to be updated.
>>>>>
>>>>> There are other extant Beans-Binding libraries, which could be
>>>>> modified to use
>>>>> this proposal, but as none of the existing features have been
>>>>> changed there
>>>>> is no need to change them (other than for type safety and
>>>>> compiler/
>>>>> IDE
>>>>> checkability).
>>>>>
>>>>> COMPATIBILITY
>>>>>
>>>>> BREAKING CHANGES:
>>>>>
>>>>> None. This change should not make any existing correct code
>>>>> fail to
>>>>> compile
>>>>> or run or change the way in which it compiles/runs.
>>>>>
>>>>> EXISTING PROGRAMS:
>>>>>
>>>>> No change required to any existing programs
>>>>>
>>>>> REFERENCES
>>>>>
>>>>> EXISTING BUGS:
>>>>>
>>>>> None
>>>>>
>>>>> URL FOR PROTOTYPE (optional):
>>>>>
>>>>> I do not have the knowledge to make changes to the compiler, and
>>>>> the
>>>>> only
>>>>> documentation making such changes concentrated on adding operators
>>>>> not
>>>>> changes at this level. So there is no prototype of the compiler
>>>>> part, but the
>>>>> Property class follows:-
>>>>>
>>>>> package java.lang.reflect;
>>>>>
>>>>> import java.beans.BeanInfo;
>>>>> import java.beans.Introspector;
>>>>> import java.beans.PropertyChangeSupport;
>>>>> import java.beans.PropertyDescriptor;
>>>>> import java.lang.reflect.Field;
>>>>> import java.lang.reflect.Method;
>>>>>
>>>>> /**
>>>>> * Property class
>>>>> * This is the support class for use with the # notation to provide
>>>>> lightweight
>>>>> * Property support for Java.
>>>>> *
>>>>> * @copyright Copyright(C) 2009 David Goodenough Linkchoose Ltd
>>>>> * @licence LPGL V2 : details of which can be found at http://
>>>>> fsf.org.
>>>>> * @author david.goodenough at linkchoose.co.uk
>>>>> *
>>>>> * @param <C> The Parent class for this field
>>>>> * @param <F> The Type of this field
>>>>> */
>>>>> public class Property<C,F> {
>>>>> private C parent;
>>>>> private Class<?> parentClass;
>>>>> private Field[] fields;
>>>>> private PropertyDescriptor[] pd = null;
>>>>> /**
>>>>> * Constructor used to create Property objects. The Parent object
>>>>> may be
>>>>> * null, but should normally be specified as it can be overridden
>>>>> anyway.
>>>>> * @param parent C object that contains the field
>>>>> * @param field Field describing this field
>>>>> */
>>>>> public Property(C parent, String ... fieldNames) {
>>>>> this.parent = parent;
>>>>> this(parent.getClass(), fieldNames);
>>>>> }
>>>>> /**
>>>>> * Constructor for unbound Properties, but also used internally
>>>>> after
>>>>> setting
>>>>> * the parent object by the bound Property objects.
>>>>> * @param parentClass Class of the parent object
>>>>> * @param fieldNames String[] of field names
>>>>> */
>>>>> public Property(Class<?>parentClass, String .. fieldNames) {
>>>>> this.parentClass = parentClass;
>>>>> fields = new Field[fieldNames.length];
>>>>> pd = new PropertyDescriptor[fieldNames.length];
>>>>> outer: for(int index = 0; index < fields.length; index++) {
>>>>> Field[]dclFields = parentClass.getDeclaredFields();
>>>>> for(Field field:dclFields) {
>>>>> if(field.getName().equals(fieldNames[index])) {
>>>>> fields[index] = field;
>>>>> field.setAccessible(true);
>>>>> try {
>>>>> BeanInfo beanInfo =
>>>>> Introspector.getBeanInfo(parent.getClass());
>>>>> PropertyDescriptor[]props =
>>>>> beanInfo.getPropertyDescriptors();
>>>>> for(PropertyDescriptor prop : props) {
>>>>> if(prop.getName().equals(field.getName())) {
>>>>> pd[index] = prop;
>>>>> break;
>>>>> }
>>>>> }
>>>>> } catch(Exception e) { /* assume can not find getter/
>>>>> setter
>>>>> */ }
>>>>> parentClass = field.getType();
>>>>> continue outer;
>>>>> }
>>>>> }
>>>>> throw new IllegalArgumentException("Field " + fieldNames[index] +
>>>>> " not found in class " +
>>>>> parentClass.getCanonicalName());
>>>>> }
>>>>> }
>>>>> /**
>>>>> * Getter from the field in the parent specified when this
>>>>> Property was
>>>>> created.
>>>>> * @see Property.get(C otherParent)
>>>>> * @return F the value of this field
>>>>> */
>>>>> public F get() {
>>>>> return get(parent);
>>>>> }
>>>>> /**
>>>>> * Getter with explicit parent.
>>>>> * This code will check see if this field is WriteOnly, and
>>>>> complain if it
>>>>> is.
>>>>> * It will then see if the use has provided am explicit getter,
>>>>> and call
>>>>> that
>>>>> * if present, otherwise it will just fetch the value through the
>>>>> Field
>>>>> provided
>>>>> * method.
>>>>> * @param otherParent C parent object
>>>>> * @return F value of the field
>>>>> */
>>>>> @SuppressWarnings("unchecked") // This should actually not be
>>>>> needed,
>>>>> // but the Field.get method is not
>>>>> typed
>>>>> public F get(C otherParent) {
>>>>> Object result = otherParent;
>>>>> try {
>>>>> for(int index = 0; index < fields.length; index++) {
>>>>>
>>>>> if(fields[index].getType().isAnnotationPresent(WriteOnly.class))
>>>>> throw new IllegalAccessException(
>>>>> "Can not get from a WriteOnly field - " +
>>>>> fields[index].getName());
>>>>> Method getter = pd[index] == null ? null :
>>>>> pd[index].getReadMethod();
>>>>> if(getter == null) result = fields[index].get(result);
>>>>> else result = getter.invoke(result);
>>>>> }
>>>>> } catch(Exception e) {
>>>>> throw new RuntimeException("Should not occur exception", e);
>>>>> }
>>>>> return (F)result;
>>>>> }
>>>>> /**
>>>>> * Setter to set the value of the field in the parent object
>>>>> declared with
>>>>> the
>>>>> * Property object
>>>>> * @param newValue F new value of this field
>>>>> */
>>>>> public void set(F newValue) {
>>>>> set(parent,newValue);
>>>>> }
>>>>> /**
>>>>> * Setter to set the value of the field to an explicit parent
>>>>> object.
>>>>> * If there is a ReadOnly annotation, then we object. If there is
>>>>> an
>>>>> explicit
>>>>> * setter then we use that, otherwise we set the field using the
>>>>> Field
>>>>> provided
>>>>> * set method and if there is a PropertyChangeSupport field,
>>>>> fire a
>>>>> property
>>>>> * change event to it.
>>>>> * We walk our way down the field chain, until we have the last
>>>>> object and
>>>>> its
>>>>> * field, and then we do the set.
>>>>> * @param parent C explicit parent object
>>>>> * @param newValue F new value for field in parent
>>>>> */
>>>>> public void set(C parent,F newValue) {
>>>>> try {
>>>>> Object last = parent;
>>>>> int index;
>>>>> for(index = 0; index < fields.length - 1; index++) {
>>>>>
>>>>> if(fields[index].getType().isAnnotationPresent(WriteOnly.class))
>>>>> throw new IllegalAccessException(
>>>>> "Can not get from a WriteOnly field - " +
>>>>> fields[index].getName());
>>>>> Method getter = pd[index] == null ? null :
>>>>> pd[index].getReadMethod();
>>>>> if(getter == null) last = fields[index].get(last);
>>>>> else last = getter.invoke(last);
>>>>> }
>>>>>
>>>>> if(fields[index].getType().isAnnotationPresent(ReadOnly.class))
>>>>> throw new IllegalAccessException(
>>>>> "Can not get from a WriteOnly field - " +
>>>>> fields[index].getName());
>>>>> Method setter = pd[index] == null ? null :
>>>>> pd[index].getWriteMethod();
>>>>> if(setter == null) {
>>>>> PropertyChangeSupport pcs = findPcs(last.getClass());
>>>>> fields[index].set(last,newValue);
>>>>> if(pcs != null)
>>>>> pcs.firePropertyChange(fields[index].getName(),
>>>>> newValue,
>>>>>
>>>>> fields[index].get(last));
>>>>> } else setter.invoke(last,newValue);
>>>>> } catch(Exception e) {
>>>>> throw new RuntimeException("Should not occur
>>>>> exception", e);
>>>>> }
>>>>> }
>>>>> /**
>>>>> * This is used so that the caller can view the Field name
>>>>> * @return String field name
>>>>> */
>>>>> public String[] getFieldName() {
>>>>> String[]names = new String[fields.length];
>>>>> for(int index = 0; index < fields.length; index++) {
>>>>> names[index] = fields[index].getName();
>>>>> }
>>>>> return names;
>>>>> }
>>>>> /**
>>>>> * This method is used to fetch the Field array, which is useful
>>>>> if you
>>>>> need to
>>>>> * access the Annotations of a field.
>>>>> * @return Field[] the array of Fields describing this Property.
>>>>> */
>>>>> public Field[] getFields() {
>>>>> return fields;
>>>>> }
>>>>> /**
>>>>> * This private method looks for a PropertyChangeSupport object in
>>>>> the
>>>>> class and
>>>>> * if one is found it will return it. It looks right the way up
>>>>> the class
>>>>> tree
>>>>> * by recurring up the superClasses.
>>>>> * @param parent Class to check for PropertyChangeSupport fields
>>>>> * @return PropertyChangeSupport first found object, or null if
>>>>> not found
>>>>> */
>>>>> private PropertyChangeSupport findPcs(Class<?> parent) {
>>>>> Field fields[] = parent.getDeclaredFields();
>>>>> for(Field field:fields) {
>>>>> field.setAccessible(true);
>>>>> try {
>>>>> if(field.getType() == PropertyChangeSupport.class)
>>>>> return (PropertyChangeSupport)field.get(parent);
>>>>> } catch(Exception e) { }
>>>>> }
>>>>> // If we did not find it then try the superclass
>>>>> Class<?>superClass = parent.getSuperclass();
>>>>> if(superClass == null) return null;
>>>>> return findPcs(parent.getClass().getSuperclass());
>>>>> }
>>>>> /**
>>>>> * This annotation is used to mark a field as WriteOnly, i.e. it
>>>>> can not
>>>>> be read.
>>>>> * This stops the automatic getter operation.
>>>>> */
>>>>> public @interface WriteOnly {
>>>>> }
>>>>> /**
>>>>> * This annotation is used to mark a field as ReadOnly, i.e. it
>>>>> can not be
>>>>> written.
>>>>> * This stops the automatic setter operation.
>>>>> */
>>>>> public @interface ReadOnly {
>>>>> }
>>>>> }
>
>
>
More information about the coin-dev
mailing list