Author: mes
Date: 2011-06-21 15:31:21 -0700 (Tue, 21 Jun 2011)
New Revision: 25841
Added:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/PayloadAccumulator.java
Removed:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyMicroListenerAdapter.java
Modified:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyEventHelperImpl.java
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyListenerAdapter.java
core3/event-impl/trunk/impl/src/main/resources/META-INF/spring/bundle-context.xml
core3/event-impl/trunk/impl/src/test/java/org/cytoscape/event/CyEventHelperTest.java
Log:
merging payload event code and removing microlistener pattern
Modified:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyEventHelperImpl.java
===================================================================
---
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyEventHelperImpl.java
2011-06-21 22:30:57 UTC (rev 25840)
+++
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyEventHelperImpl.java
2011-06-21 22:31:21 UTC (rev 25841)
@@ -35,9 +35,24 @@
package org.cytoscape.event.internal;
import org.cytoscape.event.CyEvent;
+import org.cytoscape.event.CyPayloadEvent;
import org.cytoscape.event.CyEventHelper;
-import org.cytoscape.event.CyMicroListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Random;
+import java.util.Set;
+import java.lang.reflect.Constructor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,47 +61,129 @@
private static final Logger logger =
LoggerFactory.getLogger(CyEventHelperImpl.class);
private final CyListenerAdapter normal;
- private final CyMicroListenerAdapter micro;
+ private final Map<Object,Map<Class<?>,PayloadAccumulator<?,?,?>>>
sourceAccMap;
+ private final ScheduledExecutorService payloadEventMonitor;
+ private final Set<Object> silencedSources;
+ private boolean havePayload;
- public CyEventHelperImpl(final CyListenerAdapter normal, final
CyMicroListenerAdapter micro) {
+ public CyEventHelperImpl(final CyListenerAdapter normal) {
this.normal = normal;
- this.micro = micro;
- }
+ sourceAccMap = new
HashMap<Object,Map<Class<?>,PayloadAccumulator<?,?,?>>>();
+ payloadEventMonitor =
Executors.newSingleThreadScheduledExecutor();
+ silencedSources = new HashSet<Object>();
+ havePayload = false;
- @Override public <E extends CyEvent<?>> void fireSynchronousEvent(final
E event) {
- normal.fireSynchronousEvent(event);
- }
+ // This thread just flushes any accumulated payload events.
+ // It is scheduled to run repeatedly at a fixed interval.
+ final Runnable payloadChecker = new Runnable() {
+ public void run() {
+ flushPayloadEvents();
+ }
+ };
+ payloadEventMonitor.scheduleAtFixedRate(payloadChecker,
CyEventHelper.DEFAULT_PAYLOAD_INTERVAL_MILLIS,
CyEventHelper.DEFAULT_PAYLOAD_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
+ }
-
- @Override public <E extends CyEvent<?>> void
fireAsynchronousEvent(final E event) {
- normal.fireAsynchronousEvent(event);
+ @Override
+ public <E extends CyEvent<?>> void fireEvent(final E event) {
+ // Before any external event is fired, flush any accumulated
+ // payload events. Because addEventPayload() in synchronous,
+ // all payloads should be added by the time fireEvent() is
+ // called in the client code.
+ flushPayloadEvents();
+
+ normal.fireEvent(event);
}
- @Override public <M extends CyMicroListener> M
getMicroListener(Class<M> c, Object source) {
- return micro.getMicroListener(c,source);
- }
-
- @Override public <M extends CyMicroListener> void addMicroListener(M m,
Class<M> c, Object source) {
- micro.addMicroListener(m,c,source);
- }
-
- @Override public <M extends CyMicroListener> void removeMicroListener(M
m, Class<M> c, Object source) {
- micro.removeMicroListener(m,c,source);
- }
-
- @Override public void silenceEventSource(Object eventSource) {
+ @Override
+ public void silenceEventSource(Object eventSource) {
if ( eventSource == null )
return;
logger.info("silencing event source: " +
eventSource.toString());
normal.silenceEventSource(eventSource);
- micro.silenceEventSource(eventSource);
+ silencedSources.add(eventSource);
}
- @Override public void unsilenceEventSource(Object eventSource) {
+ @Override
+ public void unsilenceEventSource(Object eventSource) {
if ( eventSource == null )
return;
logger.info("unsilencing event source: " +
eventSource.toString());
normal.unsilenceEventSource(eventSource);
- micro.unsilenceEventSource(eventSource);
+ silencedSources.remove(eventSource);
}
+
+ @Override
+ public <S,P,E extends CyPayloadEvent<S,P>> void addEventPayload(S
source, P payload, Class<E> eventType) {
+ if ( payload == null || source == null || eventType == null) {
+ logger.warn("improperly specified payload event with
source: " + source +
+ " with payload: " + payload +
+ " with event type: " +
eventType);
+ return;
+ }
+
+ if ( silencedSources.contains(source))
+ return;
+
+ synchronized (this) {
+ Map<Class<?>,PayloadAccumulator<?,?,?>> cmap =
sourceAccMap.get(source);
+ if ( cmap == null ) {
+ cmap = new
HashMap<Class<?>,PayloadAccumulator<?,?,?>>();
+ sourceAccMap.put(source,cmap);
+ }
+
+ PayloadAccumulator<S,P,E> acc =
(PayloadAccumulator<S,P,E>) cmap.get(eventType);
+
+ if ( acc == null ) {
+ try {
+ acc = new
PayloadAccumulator<S,P,E>(source, eventType);
+ cmap.put(eventType,acc);
+ } catch (NoSuchMethodException nsme) {
+ logger.warn("Unable to add payload to
event, because of missing event constructor.", nsme);
+ return;
+ }
+ }
+
+ acc.addPayload(payload);
+ havePayload = true;
+ }
+ }
+
+ public void flushPayloadEvents() {
+ List<CyPayloadEvent<?,?>> flushList;
+
+ synchronized (this) {
+
+ if ( !havePayload )
+ return;
+
+ flushList = new ArrayList<CyPayloadEvent<?,?>>();
+ havePayload = false;
+
+ for ( Object source : sourceAccMap.keySet() ) {
+ for ( PayloadAccumulator<?,?,?> acc :
sourceAccMap.get(source).values() ) {
+ try {
+ CyPayloadEvent<?,?> event =
acc.newEventInstance( source );
+ if ( event != null ) {
+ flushList.add(event);
+ }
+ } catch (Exception ie) {
+ logger.warn("Couldn't
instantiate event for source: " + source, ie);
+ }
+ }
+ }
+
+ }
+
+ // Actually fire the events outside of the synchronized block.
+ for (CyPayloadEvent<?,?> event : flushList) {
+ normal.fireEvent(event);
+ }
+ }
+
+ // Used only for unit testing to prevent the confusion of multiple
+ // threads running at once.
+ public void cleanup() {
+ payloadEventMonitor.shutdown();
+ }
}
+
Modified:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyListenerAdapter.java
===================================================================
---
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyListenerAdapter.java
2011-06-21 22:30:57 UTC (rev 25840)
+++
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyListenerAdapter.java
2011-06-21 22:31:21 UTC (rev 25841)
@@ -50,7 +50,6 @@
*/
public class CyListenerAdapter {
private static final Logger logger =
LoggerFactory.getLogger(CyListenerAdapter.class);
- private static final Executor EXEC = Executors.newCachedThreadPool();
private static final ServiceComparator serviceComparator = new
ServiceComparator();
private final Map<Class<?>,ServiceTracker> serviceTrackers;
@@ -75,19 +74,21 @@
* @param <E> The type of event.
* @param event The event object.
*/
- public <E extends CyEvent<?>> void fireSynchronousEvent(final E event) {
+ public <E extends CyEvent<?>> void fireEvent(final E event) {
+ if ( event == null )
+ return;
if ( silencedSources.contains( event.getSource() ) )
return;
-
+
final Class<?> listenerClass = event.getListenerClass();
final Object[] listeners = getListeners(listenerClass);
- if ( listeners == null ) {
+ if ( listeners == null )
return;
- }
-
+
Object lastListener = null;
+
try {
final Method method =
listenerClass.getMethod("handleEvent", event.getClass());
@@ -109,39 +110,6 @@
}
}
- /**
- * Calls each listener found in the Service Registry identified by the
listenerClass
- * and filter with the supplied event in a new thread.<p>This method
should <b>ONLY</b>
- * ever be called with a thread safe event object!</p>
- *
- * @param <E> The type of event.
- * @param event The event object.
- */
- public <E extends CyEvent> void fireAsynchronousEvent(final E event) {
-
- if ( silencedSources.contains( event.getSource() ) )
- return;
-
- final Class listenerClass = event.getListenerClass();
-
- final Object[] listeners = getListeners(listenerClass);
- if ( listeners == null ) {
- return;
- }
-
- try {
- final Method method =
listenerClass.getMethod("handleEvent", event.getClass());
-
- for (final Object listener : listeners) {
- EXEC.execute(new Runner(method, listener,
event, listenerClass));
- }
- } catch (NoSuchMethodException e) {
- // TODO should probably rethrow
- logger.error("Listener doesn't implement
\"handleEvent\" method: "
- + listenerClass.getName(), e);
- }
- }
-
private Object[] getListeners(Class<?> listenerClass) {
if ( !serviceTrackers.containsKey( listenerClass ) ) {
//logger.debug("added new service tracker for " +
listenerClass);
@@ -167,30 +135,4 @@
silencedSources.remove(eventSource);
}
-
- private static class Runner implements Runnable {
- private final Method method;
- private final Object listener;
- private final Object event;
- private final Class clazz;
-
- public Runner(final Method method, final Object listener, final
Object event, Class clazz) {
- this.method = method;
- this.listener = listener;
- this.event = event;
- this.clazz = clazz;
- }
-
- public void run() {
- try {
- method.invoke(clazz.cast(listener), event);
- } catch (IllegalAccessException e) {
- // TODO should rethrow as something
- logger.error("Listener can't execute
\"handleEvent\" method: " + clazz.getName(), e);
- } catch (InvocationTargetException e) {
- // TODO should rethrow as something
- logger.error("Listener threw exception as part
of \"handleEvent\" invocation: " + listener.toString(), e);
- }
- }
- }
}
Deleted:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyMicroListenerAdapter.java
===================================================================
---
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyMicroListenerAdapter.java
2011-06-21 22:30:57 UTC (rev 25840)
+++
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/CyMicroListenerAdapter.java
2011-06-21 22:31:21 UTC (rev 25841)
@@ -1,190 +0,0 @@
-/*
- Copyright (c) 2008, The Cytoscape Consortium (www.cytoscape.org)
-
- The Cytoscape Consortium is:
- - Institute for Systems Biology
- - University of California San Diego
- - Memorial Sloan-Kettering Cancer Center
- - Institut Pasteur
- - Agilent Technologies
-
- This library is free software; you can redistribute it and/or modify it
- under the terms of the GNU Lesser General Public License as published
- by the Free Software Foundation; either version 2.1 of the License, or
- any later version.
-
- This library is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF
- MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. The software and
- documentation provided hereunder is on an "as is" basis, and the
- Institute for Systems Biology and the Whitehead Institute
- have no obligations to provide maintenance, support,
- updates, enhancements or modifications. In no event shall the
- Institute for Systems Biology and the Whitehead Institute
- be liable to any party for direct, indirect, special,
- incidental or consequential damages, including lost profits, arising
- out of the use of this software and its documentation, even if the
- Institute for Systems Biology and the Whitehead Institute
- have been advised of the possibility of such damage. See
- the GNU Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public License
- along with this library; if not, write to the Free Software Foundation,
- Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
-*/
-package org.cytoscape.event.internal;
-
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.util.HashMap;
-import java.util.TreeSet;
-import java.util.HashSet;
-import java.util.SortedSet;
-import java.util.Map;
-import java.util.Set;
-
-import org.cytoscape.event.CyMicroListener;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-public class CyMicroListenerAdapter {
-
- private final Map<Object,Map<Class<?>,Object>> proxys;
- private final Map<Object,Map<Class<?>,SortedSet<Object>>> listeners;
- private final Map<Class<?>,Object> noOpProxies;
- private final Set<Object> silencedSources;
-
- private final static ServiceComparator serviceComparator = new
ServiceComparator();
- private final static Logger logger =
LoggerFactory.getLogger(CyMicroListenerAdapter.class);
-
- public CyMicroListenerAdapter() {
- proxys = new HashMap<Object,Map<Class<?>,Object>>();
- listeners = new
HashMap<Object,Map<Class<?>,SortedSet<Object>>>();
- noOpProxies = new HashMap<Class<?>,Object>();
- silencedSources = new HashSet<Object>();
- }
-
- public <L extends CyMicroListener> L getMicroListener(Class<L>
listenerClass, Object eventSource) {
-
- Map<Class<?>,Object> classMap = proxys.get(eventSource);
- if ( classMap == null || silencedSources.contains(eventSource) )
- return listenerClass.cast( noOpProxy(listenerClass) );
-
- Object proxy = classMap.get(listenerClass);
-
- if ( proxy == null )
- return listenerClass.cast( noOpProxy(listenerClass) );
-
- return listenerClass.cast( proxy );
- }
-
- private Object noOpProxy(final Class<?> c ) {
- Object noOp = noOpProxies.get( c );
- if ( noOp == null ) {
- noOp = Proxy.newProxyInstance(c.getClassLoader(), new
Class[] { c },
- new ListenerHandler());
- noOpProxies.put(c,noOp);
- }
- return noOp;
- }
-
- public <L extends CyMicroListener> void addMicroListener(L service,
Class<L> clazz, Object source) {
- if ( service == null ) {
- logger.warn("attempting to add null listener for
microlistener class: " + clazz);
- return;
- }
-
- if ( source == null ) {
- logger.warn("attempting to add microlistener of type: "
+ clazz + " to null source");
- return;
- }
-
- // First add the listener service to the set of services
- // for this object and class.
- if ( !listeners.containsKey(source) )
- listeners.put(source, new
HashMap<Class<?>,SortedSet<Object>>());
-
- Map<Class<?>,SortedSet<Object>> listenerMap =
listeners.get(source);
- if ( !listenerMap.containsKey(clazz) )
- listenerMap.put(clazz,new
TreeSet<Object>(serviceComparator));
-
- SortedSet<Object> listenerServices = listenerMap.get(clazz);
- listenerServices.add(service);
-
- // Now create a Proxy object for this object and class.
- if ( !proxys.containsKey(source) )
- proxys.put( source, new HashMap<Class<?>,Object>() );
-
- Map<Class<?>,Object> sourceProxys = proxys.get(source);
- if ( !sourceProxys.containsKey( clazz ) )
- sourceProxys.put( clazz,
-
Proxy.newProxyInstance(clazz.getClassLoader(),
- new Class[] {
clazz },
- new
ListenerHandler(listenerServices)));
- }
-
- public <L extends CyMicroListener> void removeMicroListener(L service,
Class<L> clazz, Object source) {
- // clean up the listeners
- Map<Class<?>,SortedSet<Object>> sourceListeners =
listeners.get(source);
- if ( sourceListeners != null ) {
- SortedSet<Object> classListeners =
sourceListeners.get(clazz);
- if ( classListeners != null ) {
- classListeners.remove(service);
- if ( classListeners.size() == 0 ) {
- sourceListeners.remove( clazz );
-
- // this gets rid of the reference to
the source object, which should
- // help with garbage collection
- if ( sourceListeners.size() == 0 )
- listeners.remove(source);
-
- // clean up the proxys
- Map<Class<?>,Object> sourceProxys =
proxys.get(source);
- if ( sourceProxys != null ) {
- sourceProxys.remove(clazz);
-
- // this gets rid of the
reference to the source object, which should
- // help with garbage collection
- if ( sourceProxys.size() == 0 )
- proxys.remove(source);
- }
- }
- }
- }
- }
-
- void silenceEventSource(Object eventSource) {
- silencedSources.add(eventSource);
- }
-
- void unsilenceEventSource(Object eventSource) {
- silencedSources.remove(eventSource);
- }
-
- // Simply iterates over the provided list of Listeners and
- // executes the specified method on each Listener.
- private static class ListenerHandler implements InvocationHandler {
- private final SortedSet<Object> ol;
-
- public ListenerHandler() {
- this.ol = null;
- }
-
- public ListenerHandler(SortedSet<Object> ol) {
- this.ol = ol;
- }
-
- public Object invoke(Object proxy, Method method, Object[]
args) throws Throwable {
- if ( ol == null )
- return null;
-
- for (final Object o : ol )
- method.invoke(o,args);
-
- return null;
- }
- }
-}
Copied:
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/PayloadAccumulator.java
(from rev 25834,
core3/event-impl/branches/nagling-events/impl/src/main/java/org/cytoscape/event/internal/PayloadAccumulator.java)
===================================================================
---
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/PayloadAccumulator.java
(rev 0)
+++
core3/event-impl/trunk/impl/src/main/java/org/cytoscape/event/internal/PayloadAccumulator.java
2011-06-21 22:31:21 UTC (rev 25841)
@@ -0,0 +1,59 @@
+
+package org.cytoscape.event.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Collection;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import org.cytoscape.event.CyPayloadEvent;
+
+class PayloadAccumulator<S,P,E extends CyPayloadEvent<S,P>> {
+
+ private List<P> payloadList;
+ private final Constructor<E> constructor;
+ private Class<?> sourceClass;
+
+ PayloadAccumulator(S source, Class<E> eventType) throws
NoSuchMethodException {
+ //System.out.println(" payload accumulator: source.getClass():
" + source + " " + source.getClass());
+
+ for ( Constructor<?> cons : eventType.getConstructors() ) {
+ Class<?>[] params = cons.getParameterTypes();
+ if ( params.length == 2 && params[1] ==
Collection.class ) {
+ sourceClass = params[0];
+ }
+ }
+
+ if ( sourceClass == null )
+ throw new IllegalArgumentException("no valid source
class found!");
+
+ constructor = eventType.getConstructor(sourceClass,
Collection.class);
+ payloadList = new ArrayList<P>();
+ }
+
+ E newEventInstance(Object source) throws InstantiationException,
IllegalAccessException, InvocationTargetException, ClassCastException {
+ if ( source == null )
+ return null;
+
+ final Collection<P> coll = getPayloadCollection();
+
+ if ( coll == null )
+ return null;
+
+ return constructor.newInstance( sourceClass.cast(source), coll
);
+ }
+
+ synchronized void addPayload(P t) {
+ if ( t != null )
+ payloadList.add(t);
+ }
+
+ synchronized private Collection<P> getPayloadCollection() {
+ if ( payloadList.isEmpty() )
+ return null;
+
+ List<P> ret = payloadList;
+ payloadList = new ArrayList<P>();
+ return ret;
+ }
+}
Modified:
core3/event-impl/trunk/impl/src/main/resources/META-INF/spring/bundle-context.xml
===================================================================
---
core3/event-impl/trunk/impl/src/main/resources/META-INF/spring/bundle-context.xml
2011-06-21 22:30:57 UTC (rev 25840)
+++
core3/event-impl/trunk/impl/src/main/resources/META-INF/spring/bundle-context.xml
2011-06-21 22:31:21 UTC (rev 25841)
@@ -19,11 +19,7 @@
<constructor-arg ref="bundleContext" />
</bean>
- <bean id="cyMicroListenerAdapter"
class="org.cytoscape.event.internal.CyMicroListenerAdapter">
- </bean>
-
<bean id="cyEventHelper"
class="org.cytoscape.event.internal.CyEventHelperImpl">
<constructor-arg ref="cyListenerAdapter" />
- <constructor-arg ref="cyMicroListenerAdapter" />
</bean>
</beans>
Modified:
core3/event-impl/trunk/impl/src/test/java/org/cytoscape/event/CyEventHelperTest.java
===================================================================
---
core3/event-impl/trunk/impl/src/test/java/org/cytoscape/event/CyEventHelperTest.java
2011-06-21 22:30:57 UTC (rev 25840)
+++
core3/event-impl/trunk/impl/src/test/java/org/cytoscape/event/CyEventHelperTest.java
2011-06-21 22:31:21 UTC (rev 25841)
@@ -51,7 +51,10 @@
import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+
/**
* DOCUMENT ME!
*/
@@ -59,16 +62,21 @@
private ServiceReference stubServiceRef;
private ServiceReference fakeServiceRef;
+ private ServiceReference payloadServiceRef;
private BundleContext bc;
+ private CyEventHelperImpl helperImpl;
/**
* DOCUMENT ME!
*/
+ @Before
public void setUp() {
service = new StubCyListenerImpl();
+ payloadService = new StubCyPayloadListenerImpl();
stubServiceRef = new MockServiceReference();
fakeServiceRef = new MockServiceReference();
+ payloadServiceRef = new MockServiceReference();
bc = new MockBundleContext() {
public ServiceReference
getServiceReference(String clazz) {
@@ -76,6 +84,8 @@
return fakeServiceRef;
else if ( clazz.equals(
StubCyListener.class.getName() ) )
return stubServiceRef;
+ else if ( clazz.equals(
StubCyPayloadListener.class.getName() ) )
+ return payloadServiceRef;
else
return null;
}
@@ -86,6 +96,8 @@
return new ServiceReference[] {
fakeServiceRef };
else if ( clazz.equals(
StubCyListener.class.getName() ) )
return new ServiceReference[] {
stubServiceRef };
+ else if ( clazz.equals(
StubCyPayloadListener.class.getName() ) )
+ return new ServiceReference[] {
payloadServiceRef };
else
return null;
}
@@ -93,14 +105,21 @@
public Object getService(ServiceReference ref) {
if ( ref == stubServiceRef )
return service;
+ else if ( ref == payloadServiceRef )
+ return payloadService;
else
return null;
}
};
CyListenerAdapter la = new CyListenerAdapter(bc);
- CyMicroListenerAdapter ma = new CyMicroListenerAdapter();
- helper = new CyEventHelperImpl(la,ma);
+ helperImpl = new CyEventHelperImpl(la);
+ helper = helperImpl;
}
+
+ @After
+ public void cleanup() {
+ helperImpl.cleanup();
+ }
}
--
You received this message because you are subscribed to the Google Groups
"cytoscape-cvs" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/cytoscape-cvs?hl=en.