AbstractEventRouter.java
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2008-2017 the original author or authors.
005  *
006  * Licensed under the Apache License, Version 2.0 (the "License");
007  * you may not use this file except in compliance with the License.
008  * You may obtain a copy of the License at
009  *
010  *     http://www.apache.org/licenses/LICENSE-2.0
011  *
012  * Unless required by applicable law or agreed to in writing, software
013  * distributed under the License is distributed on an "AS IS" BASIS,
014  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015  * See the License for the specific language governing permissions and
016  * limitations under the License.
017  */
018 package org.codehaus.griffon.runtime.core.event;
019 
020 import griffon.core.CallableWithArgs;
021 import griffon.core.ExceptionHandler;
022 import griffon.core.ExecutorServiceManager;
023 import griffon.core.RunnableWithArgs;
024 import griffon.core.event.Event;
025 import griffon.core.event.EventRouter;
026 import griffon.util.GriffonClassUtils;
027 import griffon.util.MethodDescriptor;
028 import griffon.util.MethodUtils;
029 import org.slf4j.Logger;
030 import org.slf4j.LoggerFactory;
031 
032 import javax.annotation.Nonnull;
033 import javax.annotation.Nullable;
034 import javax.inject.Inject;
035 import java.lang.reflect.Method;
036 import java.util.ArrayList;
037 import java.util.Collection;
038 import java.util.HashSet;
039 import java.util.LinkedHashMap;
040 import java.util.List;
041 import java.util.Map;
042 import java.util.Set;
043 import java.util.concurrent.ConcurrentHashMap;
044 import java.util.concurrent.ExecutorService;
045 import java.util.concurrent.Executors;
046 import java.util.concurrent.ThreadFactory;
047 import java.util.concurrent.atomic.AtomicInteger;
048 
049 import static griffon.util.GriffonClassUtils.convertToTypeArray;
050 import static griffon.util.GriffonNameUtils.capitalize;
051 import static griffon.util.GriffonNameUtils.requireNonBlank;
052 import static java.util.Arrays.asList;
053 import static java.util.Collections.EMPTY_LIST;
054 import static java.util.Collections.unmodifiableCollection;
055 import static java.util.Objects.requireNonNull;
056 
057 /**
058  @author Andres Almiray
059  */
060 public abstract class AbstractEventRouter implements EventRouter {
061     protected static final Object[] LOCK = new Object[0];
062     private static final String ERROR_EVENT_NAME_BLANK = "Argument 'eventName' must not be blank";
063     private static final String ERROR_EVENT_HANDLER_BLANK = "Argument 'eventHandler' must not be blank";
064     private static final String ERROR_MODE_BLANK = "Argument 'mode' must not be blank";
065     private static final String ERROR_LISTENER_NULL = "Argument 'listener' must not be null";
066     private static final String ERROR_EVENT_CLASS_NULL = "Argument 'eventClass' must not be null";
067     private static final String ERROR_EVENT_NULL = "Argument 'event' must not be null";
068     private static final String ERROR_CALLABLE_NULL = "Argument 'callable' must not be null";
069     private static final String ERROR_RUNNABLE_NULL = "Argument 'runnable' must not be null";
070     private static final String ERROR_PARAMS_NULL = "Argument 'params' must not be null";
071     private static final String ERROR_INSTANCE_NULL = "Argument 'instance' must not be null";
072     private static final String ERROR_OWNER_NULL = "Argument 'owner' must not be null";
073     private static final Logger LOG = LoggerFactory.getLogger(AbstractEventRouter.class);
074     protected final Map<String, List<Object>> instanceListeners = new ConcurrentHashMap<>();
075     protected final Map<String, List<Object>> functionalListeners = new ConcurrentHashMap<>();
076     private final MethodCache methodCache = new MethodCache();
077     private boolean enabled = true;
078 
079     protected static final AtomicInteger EVENT_ROUTER_ID = new AtomicInteger(1);
080 
081     protected ExecutorServiceManager executorServiceManager;
082     protected final ExecutorService executorService;
083     protected final int eventRouterId;
084 
085     @Inject
086     private ExceptionHandler exceptionHandler;
087 
088     public AbstractEventRouter() {
089         eventRouterId = EVENT_ROUTER_ID.getAndIncrement();
090         executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()new DefaultThreadFactory(eventRouterId));
091     }
092 
093     @Inject
094     public void setExecutorServiceManager(@Nonnull ExecutorServiceManager executorServiceManager) {
095         requireNonNull(executorServiceManager, "Argument 'executorServiceManager' must not be null");
096         if (this.executorServiceManager != null) {
097             this.executorServiceManager.remove(executorService);
098         }
099         this.executorServiceManager = executorServiceManager;
100         this.executorServiceManager.add(executorService);
101     }
102 
103     protected void runInsideExecutorService(@Nonnull final Runnable runnable) {
104         requireNonNull(runnable, ERROR_RUNNABLE_NULL);
105         executorService.submit(new Runnable() {
106             public void run() {
107                 try {
108                     runnable.run();
109                 catch (Throwable throwable) {
110                     exceptionHandler.uncaughtException(Thread.currentThread(), throwable);
111                 }
112             }
113         });
114     }
115 
116     @Override
117     public boolean isEventPublishingEnabled() {
118         synchronized (LOCK) {
119             return this.enabled;
120         }
121     }
122 
123     @Override
124     public void setEventPublishingEnabled(boolean enabled) {
125         synchronized (LOCK) {
126             this.enabled = enabled;
127         }
128     }
129 
130     @Override
131     public void publishEvent(@Nonnull String eventName) {
132         publishEvent(eventName, EMPTY_LIST);
133     }
134 
135     @Override
136     public void publishEvent(@Nonnull String eventName, @Nullable List<?> params) {
137         if (!isEventPublishingEnabled()) { return}
138         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
139         if (params == null) { params = EMPTY_LIST; }
140         buildPublisher(eventName, params, "synchronously").run();
141     }
142 
143     @Override
144     public void publishEventOutsideUI(@Nonnull String eventName) {
145         publishEventOutsideUI(eventName, EMPTY_LIST);
146     }
147 
148     @Override
149     public void publishEventOutsideUI(@Nonnull String eventName, @Nullable List<?> params) {
150         if (!isEventPublishingEnabled()) { return}
151         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
152         if (params == null) { params = EMPTY_LIST; }
153         final Runnable publisher = buildPublisher(eventName, params, "outside UI");
154         doPublishOutsideUI(publisher);
155     }
156 
157     protected abstract void doPublishOutsideUI(@Nonnull Runnable publisher);
158 
159     @Override
160     public void publishEventAsync(@Nonnull String eventName) {
161         publishEventAsync(eventName, EMPTY_LIST);
162     }
163 
164     @Override
165     public void publishEventAsync(@Nonnull String eventName, @Nullable List<?> params) {
166         if (!isEventPublishingEnabled()) { return}
167         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
168         if (params == null) { params = EMPTY_LIST; }
169         final Runnable publisher = buildPublisher(eventName, params, "asynchronously");
170         doPublishAsync(publisher);
171     }
172 
173     protected abstract void doPublishAsync(@Nonnull Runnable publisher);
174 
175     @Override
176     public void publishEvent(@Nonnull Event event) {
177         requireNonNull(event, ERROR_EVENT_NULL);
178         publishEvent(event.getClass().getSimpleName(), asList(event));
179     }
180 
181     @Override
182     public void publishEventOutsideUI(@Nonnull Event event) {
183         requireNonNull(event, ERROR_EVENT_NULL);
184         publishEventOutsideUI(event.getClass().getSimpleName(), asList(event));
185     }
186 
187     @Override
188     public void publishEventAsync(@Nonnull Event event) {
189         requireNonNull(event, ERROR_EVENT_NULL);
190         publishEventAsync(event.getClass().getSimpleName(), asList(event));
191     }
192 
193     @Override
194     public <E extends Event> void removeEventListener(@Nonnull Class<E> eventClass, @Nonnull CallableWithArgs<?> listener) {
195         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
196         removeEventListener(eventClass.getSimpleName(), listener);
197     }
198 
199     @Override
200     public <E extends Event> void removeEventListener(@Nonnull Class<E> eventClass, @Nonnull RunnableWithArgs listener) {
201         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
202         removeEventListener(eventClass.getSimpleName(), listener);
203     }
204 
205     protected void fireEvent(@Nonnull RunnableWithArgs runnable, @Nonnull List<?> params) {
206         requireNonNull(runnable, ERROR_RUNNABLE_NULL);
207         requireNonNull(params, ERROR_PARAMS_NULL);
208         runnable.run(asArray(params));
209     }
210 
211     protected void fireEvent(@Nonnull CallableWithArgs<?> callable, @Nonnull List<?> params) {
212         requireNonNull(callable, ERROR_CALLABLE_NULL);
213         requireNonNull(params, ERROR_PARAMS_NULL);
214         callable.call(asArray(params));
215     }
216 
217     protected void fireEvent(@Nonnull Object instance, @Nonnull String eventHandler, @Nonnull List<?> params) {
218         requireNonNull(instance, ERROR_INSTANCE_NULL);
219         requireNonBlank(eventHandler, ERROR_EVENT_HANDLER_BLANK);
220         requireNonNull(params, ERROR_PARAMS_NULL);
221 
222         Class[] argTypes = convertToTypeArray(asArray(params));
223         MethodDescriptor target = new MethodDescriptor(eventHandler, argTypes);
224         Method method = methodCache.findMatchingMethodFor(instance.getClass(), target);
225 
226         if (method != null) {
227             MethodUtils.invokeUnwrapping(method, instance, asArray(params));
228         }
229     }
230 
231     @Override
232     public <E extends Event> void addEventListener(@Nonnull Class<E> eventClass, @Nonnull CallableWithArgs<?> listener) {
233         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
234         addEventListener(eventClass.getSimpleName(), listener);
235     }
236 
237     @Override
238     public <E extends Event> void addEventListener(@Nonnull Class<E> eventClass, @Nonnull RunnableWithArgs listener) {
239         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
240         addEventListener(eventClass.getSimpleName(), listener);
241     }
242 
243     @Override
244     @SuppressWarnings("unchecked")
245     public void addEventListener(@Nonnull Object listener) {
246         requireNonNull(listener, ERROR_LISTENER_NULL);
247         if (listener instanceof RunnableWithArgs) {
248             throw new IllegalArgumentException("Cannot add an event listener of type " + RunnableWithArgs.class.getName() +
249                 " because the target event name is missing. " + listener);
250         }
251         if (listener instanceof CallableWithArgs) {
252             throw new IllegalArgumentException("Cannot add an event listener of type " + CallableWithArgs.class.getName() +
253                 " because the target event name is missing. " + listener);
254         }
255 
256         if (listener instanceof Map) {
257             addEventListener((Maplistener);
258             return;
259         }
260 
261         if (!methodCache.isEventListener(listener.getClass())) {
262             return;
263         }
264 
265         boolean added = false;
266         for (String eventName : methodCache.fetchMethodMetadata(listener.getClass()).keySet()) {
267             eventName = eventName.substring(2)// cut off "on" from the name
268             List<Object> instances = instanceListeners.get(eventName);
269             if (instances == null) {
270                 instances = new ArrayList<>();
271                 instanceListeners.put(eventName, instances);
272             }
273             synchronized (instances) {
274                 if (!instances.contains(listener)) {
275                     added = true;
276                     instances.add(listener);
277                 }
278             }
279         }
280 
281         if (added) {
282             try {
283                 LOG.debug("Adding listener {}", listener);
284             catch (UnsupportedOperationException uoe) {
285                 LOG.debug("Adding listener {}", listener.getClass().getName());
286             }
287         }
288     }
289 
290     @Override
291     public void addEventListener(@Nonnull Map<String, Object> listener) {
292         requireNonNull(listener, ERROR_LISTENER_NULL);
293         for (Map.Entry<String, Object> entry : listener.entrySet()) {
294             Object eventHandler = entry.getValue();
295             if (eventHandler instanceof RunnableWithArgs) {
296                 addEventListener(entry.getKey()(RunnableWithArgseventHandler);
297             else if (eventHandler instanceof CallableWithArgs) {
298                 addEventListener(entry.getKey()(CallableWithArgseventHandler);
299             else {
300                 throw new IllegalArgumentException("Unsupported functional event listener " + eventHandler);
301             }
302         }
303     }
304 
305     @Override
306     @SuppressWarnings("unchecked")
307     public void removeEventListener(@Nonnull Object listener) {
308         requireNonNull(listener, ERROR_LISTENER_NULL);
309         if (listener instanceof RunnableWithArgs) {
310             throw new IllegalArgumentException("Cannot remove an event listener of type " + RunnableWithArgs.class.getName() +
311                 " because the target event name is missing. " + listener);
312         }
313         if (listener instanceof CallableWithArgs) {
314             throw new IllegalArgumentException("Cannot remove an event listener of type " + CallableWithArgs.class.getName() +
315                 " because the target event name is missing. " + listener);
316         }
317 
318         if (listener instanceof Map) {
319             removeEventListener((Maplistener);
320             return;
321         }
322 
323         boolean removed = false;
324         for (String eventName : methodCache.fetchMethodMetadata(listener.getClass()).keySet()) {
325             eventName = eventName.substring(2)// cut off "on" from the name
326             List<Object> instances = instanceListeners.get(eventName);
327             if (instances != null && instances.contains(listener)) {
328                 instances.remove(listener);
329                 removed = true;
330                 if (instances.isEmpty()) {
331                     instanceListeners.remove(eventName);
332                 }
333             }
334         }
335 
336         boolean nestedRemoved = removeNestedListeners(listener);
337 
338         if (removed || nestedRemoved) {
339             try {
340                 LOG.debug("Removing listener {}", listener);
341             catch (UnsupportedOperationException uoe) {
342                 LOG.debug("Removing listener {}", listener.getClass().getName());
343             }
344         }
345     }
346 
347     @Override
348     public void removeEventListener(@Nonnull Map<String, Object> listener) {
349         requireNonNull(listener, ERROR_LISTENER_NULL);
350         for (Map.Entry<String, Object> entry : listener.entrySet()) {
351             Object eventHandler = entry.getValue();
352             if (eventHandler instanceof RunnableWithArgs) {
353                 removeEventListener(entry.getKey()(RunnableWithArgseventHandler);
354             else if (eventHandler instanceof CallableWithArgs) {
355                 removeEventListener(entry.getKey()(CallableWithArgseventHandler);
356             else {
357                 throw new IllegalArgumentException("Unsupported functional event listener " + eventHandler);
358             }
359         }
360     }
361 
362     @Override
363     public void addEventListener(@Nonnull String eventName, @Nonnull CallableWithArgs<?> listener) {
364         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
365         requireNonNull(listener, ERROR_LISTENER_NULL);
366         synchronized (functionalListeners) {
367             List<Object> list = functionalListeners.get(capitalize(eventName));
368             if (list == null) {
369                 list = new ArrayList<>();
370                 functionalListeners.put(capitalize(eventName), list);
371             }
372             if (list.contains(listener)) { return}
373             LOG.debug("Adding listener {} on {}", listener.getClass().getName(), capitalize(eventName));
374             list.add(listener);
375         }
376     }
377 
378     @Override
379     public void addEventListener(@Nonnull String eventName, @Nonnull RunnableWithArgs listener) {
380         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
381         requireNonNull(listener, ERROR_LISTENER_NULL);
382         synchronized (functionalListeners) {
383             List<Object> list = functionalListeners.get(capitalize(eventName));
384             if (list == null) {
385                 list = new ArrayList<>();
386                 functionalListeners.put(capitalize(eventName), list);
387             }
388             if (list.contains(listener)) { return}
389             LOG.debug("Adding listener {} on {}", listener.getClass().getName(), capitalize(eventName));
390             list.add(listener);
391         }
392     }
393 
394     @Override
395     public void removeEventListener(@Nonnull String eventName, @Nonnull CallableWithArgs<?> listener) {
396         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
397         requireNonNull(listener, ERROR_LISTENER_NULL);
398         synchronized (functionalListeners) {
399             List<Object> list = functionalListeners.get(capitalize(eventName));
400             if (list != null) {
401                 LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName));
402                 list.remove(listener);
403             }
404         }
405     }
406 
407     @Override
408     public void removeEventListener(@Nonnull String eventName, @Nonnull RunnableWithArgs listener) {
409         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
410         requireNonNull(listener, ERROR_LISTENER_NULL);
411         synchronized (functionalListeners) {
412             List<Object> list = functionalListeners.get(capitalize(eventName));
413             if (list != null) {
414                 LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName));
415                 list.remove(listener);
416             }
417         }
418     }
419 
420     @Nonnull
421     @Override
422     public Collection<Object> getEventListeners() {
423         List<Object> listeners = new ArrayList<>();
424         synchronized (instanceListeners) {
425             Set<Object> instances = new HashSet<>();
426             for (List<Object> objects : instanceListeners.values()) {
427                 instances.addAll(objects);
428             }
429             listeners.addAll(instances);
430         }
431 
432         synchronized (functionalListeners) {
433             for (List<Object> objects : functionalListeners.values()) {
434                 listeners.addAll(objects);
435             }
436         }
437 
438         return unmodifiableCollection(listeners);
439     }
440 
441     @Nonnull
442     @Override
443     public Collection<Object> getEventListeners(@Nonnull String eventName) {
444         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
445         List<Object> listeners = new ArrayList<>();
446         List<Object> instances = instanceListeners.get(eventName);
447         if (instances != null) { listeners.addAll(instances)}
448         instances = functionalListeners.get(eventName);
449         if (instances != null) { listeners.addAll(instances)}
450         return unmodifiableCollection(listeners);
451     }
452 
453     protected Runnable buildPublisher(@Nonnull final String event, @Nonnull final List<?> params, @Nonnull final String mode) {
454         requireNonNull(event, ERROR_EVENT_NULL);
455         requireNonNull(params, ERROR_PARAMS_NULL);
456         requireNonBlank(mode, ERROR_MODE_BLANK);
457         return new Runnable() {
458             public void run() {
459                 String eventName = capitalize(event);
460                 LOG.debug("Triggering event '{}' {}", eventName, mode);
461                 String eventHandler = "on" + eventName;
462                 // defensive copying to avoid CME during event dispatching
463                 List<Object> listenersCopy = new ArrayList<>();
464                 List<Object> instances = instanceListeners.get(eventName);
465                 if (instances != null) {
466                     listenersCopy.addAll(instances);
467                 }
468                 synchronized (functionalListeners) {
469                     List<?> list = functionalListeners.get(eventName);
470                     if (list != null) {
471                         listenersCopy.addAll(list);
472                     }
473                 }
474 
475                 for (Object listener : listenersCopy) {
476                     if (listener instanceof RunnableWithArgs) {
477                         fireEvent((RunnableWithArgslistener, params);
478                     else if (listener instanceof CallableWithArgs) {
479                         fireEvent((CallableWithArgs<?>listener, params);
480                     else {
481                         fireEvent(listener, eventHandler, params);
482                     }
483                 }
484             }
485         };
486     }
487 
488     protected boolean removeNestedListeners(@Nonnull Object owner) {
489         requireNonNull(owner, ERROR_OWNER_NULL);
490 
491         boolean removed = false;
492         synchronized (functionalListeners) {
493             for (Map.Entry<String, List<Object>> event : functionalListeners.entrySet()) {
494                 String eventName = event.getKey();
495                 List<Object> listenerList = event.getValue();
496                 List<Object> toRemove = new ArrayList<>();
497                 for (Object listener : listenerList) {
498                     if (isNestedListener(listener, owner)) {
499                         toRemove.add(listener);
500                     }
501                 }
502                 removed = !toRemove.isEmpty();
503                 for (Object listener : toRemove) {
504                     LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName));
505                     listenerList.remove(listener);
506                 }
507             }
508         }
509 
510         return removed;
511     }
512 
513     protected boolean isNestedListener(@Nonnull Object listener, @Nonnull Object owner) {
514         requireNonNull(listener, ERROR_LISTENER_NULL);
515         requireNonNull(owner, ERROR_OWNER_NULL);
516         Class<?> listenerClass = listener.getClass();
517         return (listenerClass.isMemberClass() || listenerClass.isAnonymousClass() || listenerClass.isLocalClass()) &&
518             owner.getClass().equals(listenerClass.getEnclosingClass()) &&
519             owner.equals(GriffonClassUtils.getFieldValue(listener, "this$0"));
520     }
521 
522     protected Object[] asArray(@Nonnull List<?> list) {
523         return list.toArray(new Object[list.size()]);
524     }
525 
526     protected static class MethodCache {
527         private final Map<Class<?>, Map<String, List<MethodInfo>>> methodMap = new ConcurrentHashMap<>();
528 
529         public boolean isEventListener(@Nonnull Class<?> klass) {
530             Map<String, List<MethodInfo>> methodMetadata = methodMap.get(klass);
531             if (methodMetadata == null) {
532                 methodMetadata = fetchMethodMetadata(klass);
533                 if (!methodMetadata.isEmpty()) {
534                     methodMap.put(klass, methodMetadata);
535                 else {
536                     methodMetadata = null;
537                 }
538             }
539             return methodMetadata != null;
540         }
541 
542         @Nullable
543         public Method findMatchingMethodFor(@Nonnull Class<?> klass, @Nonnull MethodDescriptor target) {
544             Map<String, List<MethodInfo>> methodMetadata = methodMap.get(klass);
545 
546             List<MethodInfo> descriptors = methodMetadata.get(target.getName());
547             if (descriptors != null) {
548                 for (MethodInfo info : descriptors) {
549                     if (info.descriptor.matches(target)) {
550                         return info.method;
551                     }
552                 }
553             }
554 
555             return null;
556         }
557 
558         private Map<String, List<MethodInfo>> fetchMethodMetadata(Class<?> klass) {
559             Map<String, List<MethodInfo>> methodMetadata = new LinkedHashMap<>();
560 
561             for (Method method : klass.getMethods()) {
562                 MethodDescriptor descriptor = MethodDescriptor.forMethod(method);
563                 if (GriffonClassUtils.isEventHandler(descriptor)) {
564                     String methodName = method.getName();
565                     List<MethodInfo> descriptors = methodMetadata.get(methodName);
566                     if (descriptors == null) {
567                         descriptors = new ArrayList<>();
568                         methodMetadata.put(methodName, descriptors);
569                     }
570                     descriptors.add(new MethodInfo(descriptor, method));
571                 }
572             }
573 
574             return methodMetadata;
575         }
576     }
577 
578     protected static class MethodInfo {
579         private final MethodDescriptor descriptor;
580         private final Method method;
581 
582         public MethodInfo(MethodDescriptor descriptor, Method method) {
583             this.descriptor = descriptor;
584             this.method = method;
585         }
586 
587         public MethodDescriptor getDescriptor() {
588             return descriptor;
589         }
590 
591         public Method getMethod() {
592             return method;
593         }
594     }
595 
596     private static class DefaultThreadFactory implements ThreadFactory {
597         private final ThreadGroup group;
598         private final AtomicInteger threadNumber = new AtomicInteger(1);
599         private final String namePrefix;
600 
601         private DefaultThreadFactory(int eventRouterId) {
602             SecurityManager s = System.getSecurityManager();
603             group = (s != null? s.getThreadGroup() :
604                 Thread.currentThread().getThreadGroup();
605             namePrefix = "event-router-" + eventRouterId + "-thread-";
606         }
607 
608         public Thread newThread(Runnable r) {
609             Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement()0);
610             if (t.isDaemon()) { t.setDaemon(false)}
611             if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY)}
612             return t;
613         }
614     }
615 }