| /******************************************************************************* |
| * Copyright (c) 2005, 2006 IBM Corporation and others. |
| * 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: |
| * IBM Corporation - initial API and implementation |
| * Brad Reynolds - bug 159539 |
| * Brad Reynolds - bug 140644 |
| * Brad Reynolds - bug 159940 |
| *******************************************************************************/ |
| package org.eclipse.jface.tests.databinding; |
| |
| import junit.framework.TestCase; |
| |
| import org.eclipse.jface.databinding.observable.IObservable; |
| import org.eclipse.jface.databinding.observable.list.IListChangeListener; |
| import org.eclipse.jface.databinding.observable.list.IObservableList; |
| import org.eclipse.jface.databinding.observable.list.ListDiff; |
| import org.eclipse.jface.databinding.observable.list.WritableList; |
| import org.eclipse.jface.databinding.observable.value.AbstractObservableValue; |
| import org.eclipse.jface.databinding.observable.value.IObservableValue; |
| import org.eclipse.jface.databinding.observable.value.IValueChangeListener; |
| import org.eclipse.jface.databinding.observable.value.ValueDiff; |
| import org.eclipse.jface.databinding.observable.value.WritableValue; |
| import org.eclipse.jface.internal.databinding.provisional.BindSpec; |
| import org.eclipse.jface.internal.databinding.provisional.Binding; |
| import org.eclipse.jface.internal.databinding.provisional.BindingAdapter; |
| import org.eclipse.jface.internal.databinding.provisional.BindingEvent; |
| import org.eclipse.jface.internal.databinding.provisional.BindingException; |
| import org.eclipse.jface.internal.databinding.provisional.DataBindingContext; |
| import org.eclipse.jface.internal.databinding.provisional.conversion.ConvertString2Byte; |
| import org.eclipse.jface.internal.databinding.provisional.conversion.IConverter; |
| import org.eclipse.jface.internal.databinding.provisional.conversion.IdentityConverter; |
| import org.eclipse.jface.internal.databinding.provisional.conversion.ToStringConverter; |
| import org.eclipse.jface.internal.databinding.provisional.description.NestedProperty; |
| import org.eclipse.jface.internal.databinding.provisional.description.Property; |
| import org.eclipse.jface.internal.databinding.provisional.factories.DefaultBindSupportFactory; |
| import org.eclipse.jface.internal.databinding.provisional.factories.IObservableFactory; |
| import org.eclipse.jface.internal.databinding.provisional.factories.NestedObservableFactory; |
| import org.eclipse.jface.internal.databinding.provisional.validation.IValidator; |
| import org.eclipse.jface.internal.databinding.provisional.validation.ValidationError; |
| import org.eclipse.jface.tests.databinding.util.Mocks; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| |
| public class DatabindingContextTest extends TestCase { |
| |
| boolean failed = false; |
| |
| DataBindingContext dbc; |
| |
| IObservableValue observableValueRMock; |
| |
| IValidator validatorMock; |
| |
| WritableValue settableValue1; |
| |
| WritableValue settableValue2; |
| |
| Object o1 = new Object(); |
| |
| Object o2 = new Object(); |
| |
| private static IConverter identityConverter = new IdentityConverter( |
| Object.class); |
| |
| protected void setUp() throws Exception { |
| super.setUp(); |
| dbc = DataBindingContext.createContext(new IObservableFactory[0]); |
| observableValueRMock = (IObservableValue) Mocks |
| .createRelaxedMock(IObservableValue.class); |
| validatorMock = (IValidator) Mocks.createMock(IValidator.class); |
| settableValue1 = new WritableValue(Object.class); |
| settableValue2 = new WritableValue(Object.class); |
| } |
| |
| protected void tearDown() throws Exception { |
| if (!failed) { |
| Mocks.verify(observableValueRMock); |
| Mocks.verify(validatorMock); |
| } |
| super.tearDown(); |
| } |
| |
| protected void runTest() throws Throwable { |
| try { |
| super.runTest(); |
| } catch (Throwable th) { |
| failed = true; |
| throw th; |
| } |
| } |
| |
| public void testRegisterForDispose() { |
| final boolean[] disposeCalled = new boolean[] { false }; |
| IObservableValue target = new WritableValue(Integer.TYPE) { |
| public void dispose() { |
| super.dispose(); |
| disposeCalled[0] = true; |
| } |
| }; |
| WritableValue model = new WritableValue(Integer.TYPE); |
| model.setValue(new Integer(12)); |
| Display display = new Display(); |
| Shell shell = new Shell(display); |
| final DataBindingContext dbc = DataBindingContext |
| .createContext(new IObservableFactory[] {}); |
| registerContextToDispose(shell, dbc); |
| dbc.registerForDispose(target); |
| dbc.registerForDispose(model); |
| dbc.bind(target, model, null); |
| assertEquals("target should now have model's value", 12, |
| ((Integer) target.getValue()).intValue()); |
| target.setValue(new Integer(9)); |
| assertEquals("model should now have target's value", 9, |
| ((Integer) model.getValue()).intValue()); |
| shell.dispose(); |
| display.dispose(); |
| assertTrue("dispose should have been called", disposeCalled[0]); |
| } |
| |
| private class DisposableObservable extends AbstractObservableValue { |
| protected Object computeValue() { |
| return null; |
| } |
| |
| public void setValue(Object value) { |
| } |
| |
| public Object getValueType() { |
| return Object.class; |
| } |
| |
| protected Object doGetValue() { |
| return null; |
| } |
| |
| boolean isDisposed = false; |
| |
| public void dispose() { |
| super.dispose(); |
| isDisposed = true; |
| } |
| |
| public boolean isDisposed() { |
| return isDisposed; |
| } |
| } |
| |
| private class DisposableObservableFactory implements IObservableFactory { |
| public IObservable createObservable(Object description) { |
| return new DisposableObservable(); |
| } |
| } |
| |
| public void testDisposeCalled() { |
| Display display = new Display(); |
| Shell shell = new Shell(display); |
| DataBindingContext dbc = DataBindingContext |
| .createContext(new IObservableFactory[] { new DisposableObservableFactory() }); |
| registerContextToDispose(shell, dbc); |
| DisposableObservable u = (DisposableObservable) dbc |
| .createObservable(null); |
| assertFalse("is not disposed", u.isDisposed()); |
| shell.dispose(); |
| display.dispose(); |
| assertTrue("is disposed", u.isDisposed()); |
| } |
| |
| private void registerContextToDispose(Shell shell, |
| final DataBindingContext dbc) { |
| shell.addDisposeListener(new DisposeListener() { |
| public void widgetDisposed(DisposeEvent e) { |
| dbc.dispose(); |
| } |
| }); |
| } |
| |
| public void testBindValueModel() { |
| Mocks.reset(observableValueRMock); |
| observableValueRMock.addValueChangeListener(null); |
| observableValueRMock.getValue(); |
| observableValueRMock.getValueType(); |
| Mocks.setLastReturnValue(observableValueRMock, Object.class); |
| validatorMock.isValid(null); |
| Mocks.startChecking(observableValueRMock); |
| Mocks.startChecking(validatorMock); |
| dbc.bind(settableValue1, observableValueRMock, new BindSpec( |
| identityConverter, identityConverter, validatorMock, null)); |
| Mocks.verify(observableValueRMock); |
| } |
| |
| public void testBindValueTarget() { |
| observableValueRMock.addValueChangeListener(null); |
| observableValueRMock.setValue(null); |
| observableValueRMock.getValue(); |
| observableValueRMock.getValueType(); |
| Mocks.setLastReturnValue(observableValueRMock, Object.class); |
| validatorMock.isValid(null); |
| Mocks.startChecking(observableValueRMock); |
| Mocks.startChecking(validatorMock); |
| dbc.bind(observableValueRMock, settableValue2, new BindSpec( |
| identityConverter, identityConverter, validatorMock, null)); |
| } |
| |
| public void testBindValuePropagation() { |
| settableValue1.setValue(o1); |
| settableValue2.setValue(o2); |
| dbc.bind(settableValue1, settableValue2, null); |
| assertEquals(o2, settableValue1.getValue()); |
| settableValue1.setValue(o1); |
| assertEquals(o1, settableValue2.getValue()); |
| settableValue2.setValue(o2); |
| assertEquals(o2, settableValue1.getValue()); |
| } |
| |
| public void testBindingListeners() { |
| final int[] calls = new int[] { 0, 0 }; |
| // this exact sequence of positions are not API and may change from |
| // release to release. |
| // This is just here to check that we got a sane sequence of pipeline |
| // positions |
| // and to catch when the sequence changes when we don't expect it to |
| // change. |
| // |
| // See BindingEvent#pipelinePosition for details. |
| final int[] pipelinePositions = new int[] { 0, 1, 2, 3, 4, 0, 2, 4, 1, |
| 0, 1, 2, 0, 2, 4, 1 }; |
| settableValue1.setValue(o1); |
| settableValue2.setValue(o2); |
| Binding binding = dbc.bind(settableValue1, settableValue2, null); |
| binding.addBindingEventListener(new BindingAdapter() { |
| public ValidationError bindingEvent(BindingEvent e) { |
| // Make sure we get the right sequence of pipeline positions |
| assertEquals("Unexpected pipeline position at call #" |
| + calls[0], pipelinePositions[calls[0]], |
| e.pipelinePosition); |
| calls[0]++; |
| return null; |
| } |
| }); |
| binding.addBindingEventListener(new BindingAdapter() { |
| public ValidationError bindingEvent(BindingEvent e) { |
| calls[1]++; |
| return null; |
| } |
| }); |
| assertEquals(o2, settableValue1.getValue()); |
| assertEquals( |
| "Both binding events should be called the same number of times", |
| calls[0], calls[1]); |
| settableValue1.setValue(o1); |
| assertEquals(o1, settableValue2.getValue()); |
| assertEquals( |
| "Both binding events should be called the same number of times", |
| calls[0], calls[1]); |
| settableValue2.setValue(o2); |
| assertEquals( |
| "Both binding events should be called the same number of times", |
| calls[0], calls[1]); |
| assertEquals(o2, settableValue1.getValue()); |
| |
| // Now test forcing an error from the event handler... |
| binding.addBindingEventListener(new BindingAdapter() { |
| public ValidationError bindingEvent(BindingEvent e) { |
| if (e.pipelinePosition == org.eclipse.jface.databinding.BindingEvent.PIPELINE_AFTER_CONVERT) { |
| return ValidationError.error("error"); |
| } |
| return null; |
| } |
| }); |
| settableValue1.setValue(o1); |
| settableValue2.setValue(o2); |
| assertEquals( |
| "Both binding events should be called the same number of times", |
| calls[0], calls[1]); |
| assertEquals("binding events should be called at least once", true, |
| calls[0] > 0); |
| } |
| |
| public void testCollectionBindingListeners() { |
| |
| WritableList v1 = new WritableList(); |
| WritableList v2 = new WritableList(); |
| |
| Binding binding = dbc.bind(v1, v2, null); |
| final int[] calls = new int[] { 0 }; |
| binding.addBindingEventListener(new BindingAdapter() { |
| public ValidationError bindingEvent(BindingEvent e) { |
| calls[0]++; |
| return null; |
| } |
| }); |
| |
| v2.add(0, "test"); |
| assertBindingCalls(calls); |
| v2.remove(0); |
| assertBindingCalls(calls); |
| v2.add(0, "test2"); |
| assertBindingCalls(calls); |
| v2.set(0, "test3"); |
| assertBindingCalls(calls); |
| } |
| |
| private void assertBindingCalls(final int[] calls) { |
| assertTrue("Should have seen some binding event calls", calls[0] > 0); |
| calls[0] = 0; |
| } |
| |
| public void testCreateNestedObservableWithArrays() { |
| // String parentObject = ""; |
| // NestedProperty nestedProperty = new NestedProperty(parentObject, new |
| // String[] {"nestedChild1", "nestedChild2", "foo"}, new Class[] |
| // {Integer.class, String.class, Float.class}); |
| // DataBindingContext ctx = DataBinding.createContext(new |
| // IObservableFactory[] {new MockObservableFactory(), new |
| // NestedObservableFactory()}); |
| // INestedObservableValue observableValue = (INestedObservableValue) |
| // ctx.createObservable(nestedProperty); |
| // assertEquals("The child IObservable does not have the right type.", |
| // Float.class, observableValue.getValueType()); |
| // |
| // observableValue = ((INestedObservableValue) |
| // observableValue.getOuterObservableValue()); |
| // assertEquals("The child IObservable does not have the right type.", |
| // String.class, observableValue.getValueType()); |
| // |
| // MockObservableValue v = ((MockObservableValue) |
| // observableValue.getOuterObservableValue()); |
| // assertEquals("The child IObservable does not have the right getter.", |
| // "nestedChild1", v.getDescription()); |
| // assertSame("The child IObservable does not have a correct parent |
| // target object.", parentObject, v.getOuterObservableValue()); |
| // assertEquals("The child IObservable does not have the right type.", |
| // Integer.class, v.getType()); |
| } |
| |
| public void testCreateNestedObservableWithPrototypeClass() { |
| // String parentObject = ""; |
| // NestedProperty nestedProperty = new NestedProperty(parentObject, |
| // "nestedChild1.nestedChild2.foo", NestedParent.class); |
| // DataBindingContext ctx = DataBinding.createContext(new |
| // IObservableFactory[] {new MockObservableFactory(), new |
| // NestedObservableFactory()}); |
| // INestedObservableValue observableValue = (INestedObservableValue) |
| // ctx.createObservable(nestedProperty); |
| // assertEquals("The child IObservable does not have the right type.", |
| // String.class, observableValue.getValueType()); |
| // |
| // observableValue = ((INestedObservableValue) |
| // observableValue.getOuterObservableValue()); |
| // assertEquals("The child IObservable does not have the right type.", |
| // NestedChild2.class, observableValue.getValueType()); |
| // |
| // MockObservableValue v = ((MockObservableValue) |
| // observableValue.getOuterObservableValue()); |
| // assertEquals("The child IObservable does not have the right getter.", |
| // "nestedChild1", v.getDescription()); |
| // assertSame("The child IObservable does not have a correct parent |
| // target object.", parentObject, v.getOuterObservableValue()); |
| // assertEquals("The child IObservable does not have the right type.", |
| // NestedChild1.class, v.getType()); |
| } |
| |
| public void testCreateNestedObservableWithPrototypeClassAndInvalidPath() { |
| String parentObject = ""; |
| NestedProperty nestedProperty = new NestedProperty(parentObject, |
| "nestedChild1.nestedChild3.foo", NestedParent.class); |
| try { |
| DataBindingContext ctx = new DataBindingContext(); |
| ctx.addObservableFactory(new MockObservableFactory()); |
| ctx.addObservableFactory(new NestedObservableFactory(ctx)); |
| ctx.createObservable(nestedProperty); |
| fail("Expected binding exception."); |
| } catch (BindingException be) { |
| } |
| } |
| |
| public void testFillBindSpecDefaultsMultipleConvertersAndValidators() throws Exception { |
| DataBindingContext dbc = new DataBindingContext(); |
| dbc.addBindSupportFactory(new DefaultBindSupportFactory()); |
| |
| BindSpec bs = new BindSpec(); |
| bs.setModelToTargetConverters(new IConverter[] { |
| null, new ToStringConverter(), null |
| }); |
| bs.setTargetToModelConverters(new IConverter[] { |
| null, new ConvertString2Byte(), null |
| }); |
| |
| dbc.fillBindSpecDefaults(dbc, bs, Object.class, Object.class); |
| |
| assertConverterType(bs, 0, IdentityConverter.class, bs.getModelToTargetConverters()); |
| assertConverterType(bs, 1, ToStringConverter.class, bs.getModelToTargetConverters()); |
| assertConverterType(bs, 2, IdentityConverter.class, bs.getModelToTargetConverters()); |
| |
| assertConverterType(bs, 0, IdentityConverter.class, bs.getTargetToModelConverters()); |
| assertConverterType(bs, 1, ConvertString2Byte.class, bs.getTargetToModelConverters()); |
| assertConverterType(bs, 2, IdentityConverter.class, bs.getTargetToModelConverters()); |
| } |
| |
| public void testWithDefaults() throws Exception { |
| org.eclipse.jface.databinding.DataBindingContext dbc = new org.eclipse.jface.databinding.DataBindingContext(); |
| assertNull("converter should not exist by default", dbc.createConverter(String.class, String.class)); |
| |
| dbc = org.eclipse.jface.databinding.DataBindingContext.withDefaults(); |
| assertNotNull(dbc); |
| assertNotNull("converter was not initialized with defaults", dbc.createConverter(String.class, String.class)); |
| } |
| |
| private void assertConverterType(BindSpec bs, int element, Class clazz, IConverter[] converters) { |
| assertEquals("model2target[" + element + "] = identity", clazz, converters[element].getClass()); |
| } |
| |
| /** |
| * Asserts that ValidationError is populated and change events are fired |
| * when a Binding that is associated with a context is in error. |
| * |
| * @throws Exception |
| */ |
| public void testValidationError() throws Exception { |
| WritableValue targetObservable = new WritableValue(String.class); |
| WritableValue modelObservable = new WritableValue(String.class); |
| |
| final String errorMessage = "error"; |
| org.eclipse.jface.databinding.DataBindingContext dbc = org.eclipse.jface.databinding.DataBindingContext.withDefaults(); |
| ValueChangeCounter errorCounter = new ValueChangeCounter(); |
| ListChangeCounter errorsCounter = new ListChangeCounter(); |
| |
| IObservableValue error = dbc.getValidationError(); |
| error.addValueChangeListener(errorCounter); |
| assertNull(error.getValue()); |
| |
| IObservableList errors = dbc.getValidationErrors(); |
| errors.addListChangeListener(errorsCounter); |
| assertEquals(0, errors.size()); |
| |
| IValidator validator = new IValidator() { |
| public ValidationError isPartiallyValid(Object value) { |
| return null; |
| } |
| |
| public ValidationError isValid(Object value) { |
| return ValidationError.error(errorMessage); |
| } |
| }; |
| |
| dbc.bindValue(targetObservable, |
| modelObservable, |
| new org.eclipse.jface.databinding.BindSpec().setValidator(validator)); |
| |
| targetObservable.setValue(""); |
| assertNotNull(error.getValue()); |
| assertEquals(errorMessage, error.getValue().toString()); |
| assertEquals(1, errors.size()); |
| assertEquals(1, errorsCounter.count); |
| assertEquals(1, errorCounter.count); |
| } |
| |
| /** |
| * Asserts that then |
| * {@link DataBindingContext#bindValue(IObservableValue, IObservableValue, org.eclipse.jface.databinding.BindSpec)} |
| * if invoked the created binding is added to the internal list of bindings. |
| * |
| * @throws Exception |
| */ |
| public void testBindValueAddBinding() throws Exception { |
| WritableValue targetValue = new WritableValue(String.class); |
| WritableValue modelValue = new WritableValue(String.class); |
| |
| org.eclipse.jface.databinding.DataBindingContext dbc = org.eclipse.jface.databinding.DataBindingContext.withDefaults(); |
| assertNotNull(dbc.getBindings()); |
| assertEquals(0, dbc.getBindings().size()); |
| |
| org.eclipse.jface.databinding.Binding binding = dbc.bindValue(targetValue, modelValue, null); |
| assertNotNull(binding); |
| assertNotNull(dbc.getBindings()); |
| assertEquals(1, dbc.getBindings().size()); |
| assertEquals(binding, dbc.getBindings().get(0)); |
| } |
| |
| /** |
| * Asserts that when |
| * {@link DataBindingContext#bindList(IObservableList, IObservableList, org.eclipse.jface.databinding.BindSpec)} |
| * is invoked the created binding is added to the intenal list of bindings. |
| * |
| * @throws Exception |
| */ |
| public void testBindListAddBinding() throws Exception { |
| WritableList targetList = new WritableList(Object.class); |
| WritableList modelList = new WritableList(Object.class); |
| |
| org.eclipse.jface.databinding.DataBindingContext dbc = org.eclipse.jface.databinding.DataBindingContext.withDefaults(); |
| assertNotNull(dbc.getBindings()); |
| assertEquals(0, dbc.getBindings().size()); |
| |
| org.eclipse.jface.databinding.Binding binding = dbc.bindList(targetList, modelList, null); |
| assertNotNull(binding); |
| assertNotNull(dbc.getBindings()); |
| assertEquals(1, dbc.getBindings().size()); |
| assertEquals(binding, dbc.getBindings().get(0)); |
| } |
| |
| public void testGetBindingsImmutability() throws Exception { |
| org.eclipse.jface.databinding.DataBindingContext dbc = org.eclipse.jface.databinding.DataBindingContext.withDefaults(); |
| BindingStub binding = new BindingStub(null); |
| dbc.addBinding(binding); |
| |
| try { |
| dbc.getBindings().remove(0); |
| fail("exception should have been thrown"); |
| } catch (UnsupportedOperationException e) { |
| } |
| } |
| |
| public void testRemoveBinding() throws Exception { |
| BindingStub binding = new BindingStub(null); |
| org.eclipse.jface.databinding.DataBindingContext dbc = org.eclipse.jface.databinding.DataBindingContext.withDefaults(); |
| dbc.addBinding(binding); |
| |
| assertTrue("context should contain the binding", dbc.getBindings().contains(binding)); |
| assertEquals("context should have been set on the binding", dbc, binding.context); |
| assertTrue("removing the factory should return true", dbc.removeBinding(binding)); |
| assertNull("context should have been removed", binding.context); |
| assertFalse("binding should have been removed", dbc.getBindings().contains(binding)); |
| assertFalse("when not found false should be returned", dbc.removeBinding(binding)); |
| } |
| |
| /** |
| * {@link IValueChangeListener} implementation that counts the times |
| * handleValueChange(...) is invoked. |
| * |
| * @since 3.2 |
| */ |
| private static class ValueChangeCounter implements IValueChangeListener { |
| int count; |
| |
| public void handleValueChange(IObservableValue source, ValueDiff diff) { |
| count++; |
| } |
| } |
| |
| /** |
| * {@link IListChangeListener} implementation that counts the times |
| * handleListChange(...) is invoked. |
| * |
| */ |
| private static class ListChangeCounter implements IListChangeListener { |
| int count; |
| |
| public void handleListChange(IObservableList source, ListDiff diff) { |
| count++; |
| } |
| } |
| |
| private static class BindingStub extends org.eclipse.jface.databinding.Binding { |
| org.eclipse.jface.databinding.DataBindingContext context; |
| |
| public BindingStub(org.eclipse.jface.databinding.DataBindingContext context) { |
| super(context); |
| } |
| |
| public IObservableValue getPartialValidationError() { |
| return null; |
| } |
| |
| public IObservableValue getValidationError() { |
| return null; |
| } |
| |
| public void updateModelFromTarget() { |
| } |
| |
| public void updateTargetFromModel() { |
| } |
| |
| public void setDataBindingContext(org.eclipse.jface.databinding.DataBindingContext context) { |
| this.context = context; |
| } |
| } |
| |
| //------------------------------------------------------------------------- |
| // Fixture classes |
| //------------------------------------------------------------------------- |
| |
| public class MockObservableFactory implements IObservableFactory { |
| public IObservable createObservable(Object description) { |
| Property property = (Property) description; |
| return new MockObservableValue(property.getObject(), property |
| .getPropertyID(), property.getPropertyType()); |
| } |
| } |
| |
| public class MockObservableValue extends AbstractObservableValue { |
| public Object targetObject; |
| |
| public Object description; |
| |
| private Class type; |
| |
| public MockObservableValue(Object targetObject, Object description, |
| Class type) { |
| super(); |
| this.targetObject = targetObject; |
| this.description = description; |
| this.type = type; |
| } |
| |
| public Object getDescription() { |
| return description; |
| } |
| |
| public Class getType() { |
| return type; |
| } |
| |
| public Object getOuterObservableValue() { |
| return targetObject; |
| } |
| |
| public Object computeValue() { |
| return null; |
| } |
| |
| public Object getValueType() { |
| return null; |
| } |
| |
| public void setValue(Object value) { |
| } |
| |
| protected Object doGetValue() { |
| return null; |
| } |
| |
| } |
| |
| private class NestedParent { |
| public NestedChild1 getNestedChild1() { |
| return new NestedChild1(); |
| } |
| } |
| |
| private class NestedChild1 { |
| public NestedChild2 getNestedChild2() { |
| return new NestedChild2(); |
| } |
| } |
| |
| private class NestedChild2 { |
| public String getFoo() { |
| return "foo"; |
| } |
| } |
| |
| |
| } |