DefaultMVCGroupManager.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.mvc;
019 
020 import griffon.core.ApplicationClassLoader;
021 import griffon.core.ApplicationEvent;
022 import griffon.core.GriffonApplication;
023 import griffon.core.artifact.ArtifactManager;
024 import griffon.core.artifact.GriffonArtifact;
025 import griffon.core.artifact.GriffonClass;
026 import griffon.core.artifact.GriffonController;
027 import griffon.core.artifact.GriffonMvcArtifact;
028 import griffon.core.artifact.GriffonView;
029 import griffon.core.editors.ExtendedPropertyEditor;
030 import griffon.core.editors.PropertyEditorResolver;
031 import griffon.core.mvc.MVCGroup;
032 import griffon.core.mvc.MVCGroupConfiguration;
033 import griffon.exceptions.FieldException;
034 import griffon.exceptions.GriffonException;
035 import griffon.exceptions.GriffonViewInitializationException;
036 import griffon.exceptions.MVCGroupInstantiationException;
037 import griffon.exceptions.NewInstanceException;
038 import griffon.inject.Contextual;
039 import griffon.inject.MVCMember;
040 import griffon.util.CollectionUtils;
041 import griffon.util.Instantiator;
042 import org.codehaus.griffon.runtime.core.injection.InjectionUnitOfWork;
043 import org.slf4j.Logger;
044 import org.slf4j.LoggerFactory;
045 
046 import javax.annotation.Nonnull;
047 import javax.annotation.Nullable;
048 import javax.inject.Inject;
049 import java.beans.PropertyDescriptor;
050 import java.beans.PropertyEditor;
051 import java.lang.reflect.AnnotatedElement;
052 import java.lang.reflect.Field;
053 import java.lang.reflect.Method;
054 import java.lang.reflect.Modifier;
055 import java.util.ArrayList;
056 import java.util.Arrays;
057 import java.util.LinkedHashMap;
058 import java.util.List;
059 import java.util.Map;
060 
061 import static griffon.core.GriffonExceptionHandler.sanitize;
062 import static griffon.core.editors.PropertyEditorResolver.findEditor;
063 import static griffon.util.AnnotationUtils.annotationsOfMethodParameter;
064 import static griffon.util.AnnotationUtils.findAnnotation;
065 import static griffon.util.AnnotationUtils.namesFor;
066 import static griffon.util.AnnotationUtils.parameterTypeAt;
067 import static griffon.util.ConfigUtils.getConfigValueAsBoolean;
068 import static griffon.util.GriffonClassUtils.getAllDeclaredFields;
069 import static griffon.util.GriffonClassUtils.getPropertyDescriptors;
070 import static griffon.util.GriffonClassUtils.setFieldValue;
071 import static griffon.util.GriffonClassUtils.setPropertiesOrFieldsNoException;
072 import static griffon.util.GriffonClassUtils.setPropertyOrFieldValueNoException;
073 import static griffon.util.GriffonNameUtils.capitalize;
074 import static griffon.util.GriffonNameUtils.isBlank;
075 import static java.util.Arrays.asList;
076 import static java.util.Objects.requireNonNull;
077 
078 /**
079  * Base implementation of the {@code MVCGroupManager} interface.
080  *
081  @author Andres Almiray
082  @since 2.0.0
083  */
084 public class DefaultMVCGroupManager extends AbstractMVCGroupManager {
085     private static final Logger LOG = LoggerFactory.getLogger(DefaultMVCGroupManager.class);
086 
087     protected static final String ERROR_VALUE_NULL = "Argument 'value' must not be null";
088     protected static final String CONFIG_KEY_COMPONENT = "component";
089     protected static final String CONFIG_KEY_EVENTS_LIFECYCLE = "events.lifecycle";
090     protected static final String CONFIG_KEY_EVENTS_INSTANTIATION = "events.instantiation";
091     protected static final String CONFIG_KEY_EVENTS_DESTRUCTION = "events.destruction";
092     protected static final String CONFIG_KEY_EVENTS_LISTENER = "events.listener";
093     protected static final String KEY_PARENT_GROUP = "parentGroup";
094 
095     protected final ApplicationClassLoader applicationClassLoader;
096     protected final Instantiator instantiator;
097 
098     @Inject
099     public DefaultMVCGroupManager(@Nonnull GriffonApplication application, @Nonnull ApplicationClassLoader applicationClassLoader, @Nonnull Instantiator instantiator) {
100         super(application);
101         this.applicationClassLoader = requireNonNull(applicationClassLoader, "Argument 'applicationClassLoader' must not be null");
102         this.instantiator = requireNonNull(instantiator, "Argument 'instantiator' must not be null");
103     }
104 
105     protected void doInitialize(@Nonnull Map<String, MVCGroupConfiguration> configurations) {
106         requireNonNull(configurations, "Argument 'configurations' must not be null");
107         for (MVCGroupConfiguration configuration : configurations.values()) {
108             addConfiguration(configuration);
109         }
110     }
111 
112     @Nonnull
113     protected MVCGroup createMVCGroup(@Nonnull MVCGroupConfiguration configuration, @Nullable String mvcId, @Nonnull Map<String, Object> args) {
114         requireNonNull(configuration, ERROR_CONFIGURATION_NULL);
115         requireNonNull(args, ERROR_ARGS_NULL);
116 
117         mvcId = resolveMvcId(configuration, mvcId);
118         checkIdIsUnique(mvcId, configuration);
119 
120         LOG.debug("Building MVC group '{}' with name '{}'", configuration.getMvcType(), mvcId);
121         Map<String, Object> argsCopy = copyAndConfigureArguments(args, configuration, mvcId);
122 
123         // figure out what the classes are
124         Map<String, ClassHolder> classMap = new LinkedHashMap<>();
125         for (Map.Entry<String, String> memberEntry : configuration.getMembers().entrySet()) {
126             String memberType = memberEntry.getKey();
127             String memberClassName = memberEntry.getValue();
128             selectClassesPerMember(memberType, memberClassName, classMap);
129         }
130 
131         boolean isEventPublishingEnabled = getApplication().getEventRouter().isEventPublishingEnabled();
132         getApplication().getEventRouter().setEventPublishingEnabled(isConfigFlagEnabled(configuration, CONFIG_KEY_EVENTS_INSTANTIATION));
133         Map<String, Object> instances = new LinkedHashMap<>();
134         List<Object> injectedInstances = new ArrayList<>();
135 
136         try {
137             InjectionUnitOfWork.start();
138         catch (IllegalStateException ise) {
139             throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() "' with id '" + mvcId + "'", configuration.getMvcType(), mvcId, ise);
140         }
141 
142         try {
143             instances.putAll(instantiateMembers(classMap, argsCopy));
144         finally {
145             getApplication().getEventRouter().setEventPublishingEnabled(isEventPublishingEnabled);
146             try {
147                 injectedInstances.addAll(InjectionUnitOfWork.finish());
148             catch (IllegalStateException ise) {
149                 throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() "' with id '" + mvcId + "'", configuration.getMvcType(), mvcId, ise);
150             }
151         }
152 
153         MVCGroup group = newMVCGroup(configuration, mvcId, instances, (MVCGroupargs.get(KEY_PARENT_GROUP));
154         adjustMvcArguments(group, argsCopy);
155 
156         boolean fireEvents = isConfigFlagEnabled(configuration, CONFIG_KEY_EVENTS_LIFECYCLE);
157         if (fireEvents) {
158             getApplication().getEventRouter().publishEvent(ApplicationEvent.INITIALIZE_MVC_GROUP.getName(), asList(configuration, group));
159         }
160 
161         // special case -- controllers are added as application listeners
162         if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LISTENER)) {
163             GriffonController controller = group.getController();
164             if (controller != null) {
165                 getApplication().getEventRouter().addEventListener(controller);
166             }
167         }
168 
169         // mutually set each other to the available fields and inject args
170         fillReferencedProperties(group, argsCopy);
171 
172         doAddGroup(group);
173 
174         initializeMembers(group, argsCopy);
175         if (group instanceof AbstractMVCGroup) {
176             ((AbstractMVCGroupgroup).getInjectedInstances().addAll(injectedInstances);
177         }
178 
179         if (fireEvents) {
180             getApplication().getEventRouter().publishEvent(ApplicationEvent.CREATE_MVC_GROUP.getName(), asList(group));
181         }
182 
183         return group;
184     }
185 
186     protected void adjustMvcArguments(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
187         // must set it again because mvcId might have been initialized internally
188         args.put("mvcId", group.getMvcId());
189         args.put("mvcGroup", group);
190         args.put("application", getApplication());
191     }
192 
193     @Nonnull
194     @SuppressWarnings("ConstantConditions")
195     protected String resolveMvcId(@Nonnull MVCGroupConfiguration configuration, @Nullable String mvcId) {
196         boolean component = getConfigValueAsBoolean(configuration.getConfig(), CONFIG_KEY_COMPONENT, false);
197 
198         if (isBlank(mvcId)) {
199             if (component) {
200                 mvcId = configuration.getMvcType() "-" + System.nanoTime();
201             else {
202                 mvcId = configuration.getMvcType();
203             }
204         }
205         return mvcId;
206     }
207 
208     @SuppressWarnings("unchecked")
209     protected void selectClassesPerMember(@Nonnull String memberType, @Nonnull String memberClassName, @Nonnull Map<String, ClassHolder> classMap) {
210         GriffonClass griffonClass = getApplication().getArtifactManager().findGriffonClass(memberClassName);
211         ClassHolder classHolder = new ClassHolder();
212         if (griffonClass != null) {
213             classHolder.artifactClass = (Class<? extends GriffonArtifact>griffonClass.getClazz();
214         else {
215             classHolder.regularClass = loadClass(memberClassName);
216         }
217         classMap.put(memberType, classHolder);
218     }
219 
220     @Nonnull
221     protected Map<String, Object> copyAndConfigureArguments(@Nonnull Map<String, Object> args, @Nonnull MVCGroupConfiguration configuration, @Nonnull String mvcId) {
222         Map<String, Object> argsCopy = CollectionUtils.<String, Object>map()
223             .e("application", getApplication())
224             .e("mvcType", configuration.getMvcType())
225             .e("mvcId", mvcId)
226             .e("configuration", configuration);
227 
228         if (args.containsKey(KEY_PARENT_GROUP&& args.get(KEY_PARENT_GROUPinstanceof MVCGroup) {
229             MVCGroup parentGroup = (MVCGroupargs.get(KEY_PARENT_GROUP);
230             for (Map.Entry<String, Object> e : parentGroup.getMembers().entrySet()) {
231                 args.put("parent" + capitalize(e.getKey()), e.getValue());
232             }
233         }
234 
235         argsCopy.putAll(args);
236         return argsCopy;
237     }
238 
239     protected void checkIdIsUnique(@Nonnull String mvcId, @Nonnull MVCGroupConfiguration configuration) {
240         if (findGroup(mvcId!= null) {
241             String action = getApplication().getConfiguration().getAsString("griffon.mvcid.collision""exception");
242             if ("warning".equalsIgnoreCase(action)) {
243                 LOG.warn("A previous instance of MVC group '{}' with id '{}' exists. Destroying the old instance first.", configuration.getMvcType(), mvcId);
244                 destroyMVCGroup(mvcId);
245             else {
246                 throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() "' with id '" + mvcId + "' because a previous instance with that name exists and was not disposed off properly.", configuration.getMvcType(), mvcId);
247             }
248         }
249     }
250 
251     @Nonnull
252     protected Map<String, Object> instantiateMembers(@Nonnull Map<String, ClassHolder> classMap, @Nonnull Map<String, Object> args) {
253         // instantiate the parts
254         Map<String, Object> instanceMap = new LinkedHashMap<>();
255         for (Map.Entry<String, ClassHolder> classEntry : classMap.entrySet()) {
256             String memberType = classEntry.getKey();
257             if (args.containsKey(memberType)) {
258                 // use provided value, even if null
259                 instanceMap.put(memberType, args.get(memberType));
260             else {
261                 // otherwise create a new value
262                 ClassHolder classHolder = classEntry.getValue();
263                 if (classHolder.artifactClass != null) {
264                     Class<? extends GriffonArtifact> memberClass = classHolder.artifactClass;
265                     ArtifactManager artifactManager = getApplication().getArtifactManager();
266                     GriffonClass griffonClass = artifactManager.findGriffonClass(memberClass);
267                     GriffonArtifact instance = artifactManager.newInstance(griffonClass);
268                     instanceMap.put(memberType, instance);
269                     args.put(memberType, instance);
270                 else {
271                     Class<?> memberClass = classHolder.regularClass;
272                     try {
273                         Object instance = instantiator.instantiate(memberClass);
274                         instanceMap.put(memberType, instance);
275                         args.put(memberType, instance);
276                     catch (RuntimeException e) {
277                         LOG.error("Can't create member {} with {}", memberType, memberClass);
278                         throw new NewInstanceException(memberClass, e);
279                     }
280                 }
281             }
282         }
283         return instanceMap;
284     }
285 
286     protected void initializeMembers(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
287         LOG.debug("Initializing each MVC member of group '{}'", group.getMvcId());
288         for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
289             String memberType = memberEntry.getKey();
290             Object member = memberEntry.getValue();
291             if (member instanceof GriffonArtifact) {
292                 initializeArtifactMember(group, memberType, (GriffonArtifactmember, args);
293             else {
294                 initializeNonArtifactMember(group, memberType, member, args);
295             }
296         }
297     }
298 
299     protected void initializeArtifactMember(@Nonnull final MVCGroup group, @Nonnull String type, @Nonnull final GriffonArtifact member, @Nonnull final Map<String, Object> args) {
300         if (member instanceof GriffonView) {
301             getApplication().getUIThreadManager().runInsideUISync(new Runnable() {
302                 @Override
303                 public void run() {
304                     try {
305                         GriffonView view = (GriffonViewmember;
306                         view.initUI();
307                     catch (RuntimeException e) {
308                         throw (RuntimeExceptionsanitize(new GriffonViewInitializationException(group.getMvcType(), group.getMvcId(), member.getClass().getName(), e));
309                     }
310                     ((GriffonMvcArtifactmember).mvcGroupInit(args);
311                 }
312             });
313         else if (member instanceof GriffonMvcArtifact) {
314             ((GriffonMvcArtifactmember).mvcGroupInit(args);
315         }
316     }
317 
318     protected void initializeNonArtifactMember(@Nonnull MVCGroup group, @Nonnull String type, @Nonnull Object member, @Nonnull Map<String, Object> args) {
319         // empty
320     }
321 
322     protected abstract static class InjectionPoint {
323         protected final String name;
324         protected final boolean nullable;
325         protected final Kind kind;
326         protected final Class<?> type;
327         protected final String format;
328         protected final Class<? extends PropertyEditor> editor;
329 
330         protected InjectionPoint(String name, boolean nullable, Kind kind, Class<?> type, String format, Class<? extends PropertyEditor> editor) {
331             this.name = name;
332             this.nullable = nullable;
333             this.kind = kind;
334             this.type = type; this.format = format;
335             this.editor = editor;
336         }
337 
338         protected enum Kind {
339             MEMBER,
340             CONTEXTUAL,
341             OTHER
342         }
343 
344         protected abstract void apply(@Nonnull MVCGroup group, @Nonnull String memberType, @Nonnull Object instance, @Nonnull Map<String, Object> args);
345 
346         @Nonnull
347         protected Object convertValue(@Nonnull Class<?> type, @Nonnull Object value, @Nullable String format, @Nonnull Class<? extends PropertyEditor> editor) {
348             requireNonNull(type, ERROR_TYPE_NULL);
349             requireNonNull(value, ERROR_VALUE_NULL);
350 
351             PropertyEditor propertyEditor = resolvePropertyEditor(type, format, editor);
352             if (isNoopPropertyEditor(propertyEditor.getClass())) { return value; }
353             if (value instanceof CharSequence) {
354                 propertyEditor.setAsText(String.valueOf(value));
355             else {
356                 propertyEditor.setValue(value);
357             }
358             return propertyEditor.getValue();
359         }
360 
361         @Nonnull
362         protected PropertyEditor resolvePropertyEditor(@Nonnull Class<?> type, @Nullable String format, @Nonnull Class<? extends PropertyEditor> editor) {
363             requireNonNull(type, ERROR_TYPE_NULL);
364 
365             PropertyEditor propertyEditor = null;
366             if (isNoopPropertyEditor(editor)) {
367                 propertyEditor = findEditor(type);
368             else {
369                 try {
370                     propertyEditor = editor.newInstance();
371                 catch (InstantiationException | IllegalAccessException e) {
372                     throw new GriffonException("Could not instantiate editor with " + editor, e);
373                 }
374             }
375 
376             if (propertyEditor instanceof ExtendedPropertyEditor) {
377                 ((ExtendedPropertyEditorpropertyEditor).setFormat(format);
378             }
379             return propertyEditor;
380         }
381 
382         protected boolean isNoopPropertyEditor(@Nonnull Class<? extends PropertyEditor> editor) {
383             return PropertyEditorResolver.NoopPropertyEditor.class.isAssignableFrom(editor);
384         }
385     }
386 
387     protected static class FieldInjectionPoint extends InjectionPoint {
388         protected final Field field;
389 
390         protected FieldInjectionPoint(String name, boolean nullable, Kind kind, Class<?> type, Field field, String format, Class<? extends PropertyEditor> editor) {
391             super(name, nullable, kind, type, format, editor);
392             this.field = field;
393         }
394 
395         @Override
396         protected void apply(@Nonnull MVCGroup group, @Nonnull String memberType, @Nonnull Object instance, @Nonnull Map<String, Object> args) {
397             String[] keys = namesFor(field);
398             Object argValue = args.get(name);
399 
400             if (kind == Kind.CONTEXTUAL) {
401                 for (String key : keys) {
402                     if (group.getContext().containsKey(key)) {
403                         argValue = group.getContext().get(key);
404                         break;
405                     }
406                 }
407             }
408 
409             try {
410                 if (argValue == null) {
411                     if (!nullable) {
412                         if (kind == Kind.CONTEXTUAL) {
413                             throw new IllegalStateException("Could not find an instance of type " +
414                                 field.getType().getName() " under keys '" + Arrays.toString(keys+
415                                 "' in the context of MVCGroup[" + group.getMvcType() ":" + group.getMvcId() +
416                                 "] to be injected on field '" + field.getName() +
417                                 "' in " + kind + " (" + resolveMemberClass(instance).getName() "). Field does not accept null values.");
418                         else if (kind == Kind.MEMBER) {
419                             throw new IllegalStateException("Could not inject argument on field '"
420                                 + name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
421                                 "). Field does not accept null values.");
422                         }
423                     }
424                     return;
425                 else if (kind == Kind.MEMBER && (!isNoopPropertyEditor(editor|| !type.isAssignableFrom(argValue.getClass()))) {
426                     argValue = convertValue(type, argValue, format, editor);
427                 }
428 
429                 setFieldValue(instance, name, argValue);
430                 if (kind == Kind.OTHER) {
431                     LOG.warn("Field '" + name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
432                         ") must be annotated with @" + MVCMember.class.getName() ".");
433                 }
434             catch (Exception e) {
435                 throw new MVCGroupInstantiationException("Unexpected error when setting value for " + resolveMemberClass(instance).getName() "." + field.getName(), group.getMvcType(), group.getMvcId(), e);
436             }
437         }
438     }
439 
440     protected static class MethodInjectionPoint extends InjectionPoint {
441         protected final Method method;
442 
443         protected MethodInjectionPoint(String name, boolean nullable, Kind kind, Class<?> type, Method method, String format, Class<? extends PropertyEditor> editor) {
444             super(name, nullable, kind, type, format, editor);
445             this.method = method;
446         }
447 
448         @Override
449         protected void apply(@Nonnull MVCGroup group, @Nonnull String memberType, @Nonnull Object instance, @Nonnull Map<String, Object> args) {
450             if (kind == Kind.CONTEXTUAL) {
451                 String[] keys = namesFor(method);
452                 Object argValue = args.get(name);
453 
454                 for (String key : keys) {
455                     if (group.getContext().containsKey(key)) {
456                         argValue = group.getContext().get(key);
457                         break;
458                     }
459                 }
460 
461                 try {
462                     if (argValue == null && !nullable) {
463                         throw new IllegalStateException("Could not find an instance of type " +
464                             method.getParameterTypes()[0].getName() " under keys '" + Arrays.toString(keys+
465                             "' in the context of MVCGroup[" + group.getMvcType() ":" + group.getMvcId() +
466                             "] to be injected on property '" + name +
467                             "' in " + kind + " (" + resolveMemberClass(instance).getName() "). Property does not accept null values.");
468                     }
469 
470                     method.invoke(instance, argValue);
471                 catch (Exception e) {
472                     throw new MVCGroupInstantiationException(group.getMvcType(), group.getMvcId(), e);
473                 }
474             else {
475                 try {
476                     Object argValue = args.get(name);
477                     if (argValue == null) {
478                         if (!nullable) {
479                             if (kind == Kind.MEMBER) {
480                                 throw new IllegalStateException("Could not inject argument on property '" +
481                                     name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
482                                     "). Property does not accept null values.");
483                             }
484                         }
485                         return;
486                     else if (kind == Kind.MEMBER && (!isNoopPropertyEditor(editor|| !type.isAssignableFrom(argValue.getClass()))) {
487                         argValue = convertValue(type, argValue, format, editor);
488                     }
489 
490                     method.invoke(instance, argValue);
491                     if (kind == Kind.OTHER) {
492                         LOG.warn("Property '" + name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
493                             ") must be annotated with @" + MVCMember.class.getName() ".");
494                     }
495                 catch (Exception e) {
496                     throw new MVCGroupInstantiationException("Unexpected error when invoking " + resolveMemberClass(instance).getName() "." + method.getName(), group.getMvcType(), group.getMvcId(), e);
497                 }
498             }
499         }
500     }
501 
502     protected void fillReferencedProperties(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
503         for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
504             String memberType = memberEntry.getKey();
505             Object member = memberEntry.getValue();
506 
507             Map<String, Object> argsCopy = new LinkedHashMap<>(args);
508 
509             Map<String, Field> fields = new LinkedHashMap<>();
510             for (Field field : getAllDeclaredFields(resolveMemberClass(member))) {
511                 fields.put(field.getName(), field);
512             }
513             Map<String, InjectionPoint> injectionPoints = new LinkedHashMap<>();
514             for (PropertyDescriptor descriptor : getPropertyDescriptors(resolveMemberClass(member))) {
515                 Method method = descriptor.getWriteMethod();
516                 if (method == null || isInjectable(method)) { continue}
517                 Class<?> type = parameterTypeAt(method, 0);
518                 boolean nullable = method.getAnnotation(Nonnull.class== null && findAnnotation(annotationsOfMethodParameter(method, 0), Nonnull.class== null;
519                 InjectionPoint.Kind kind = resolveKind(method);
520                 String format = resolveFormat(method);
521                 Class<? extends PropertyEditor> editor = resolveEditor(method);
522                 Field field = fields.get(descriptor.getName());
523                 if (field != null && kind == InjectionPoint.Kind.OTHER) {
524                     kind = resolveKind(field);
525                     nullable = field.getAnnotation(Nonnull.class== null;
526                     type = field.getType();
527                     format = resolveFormat(field);
528                     editor = resolveEditor(field);
529                 }
530                 injectionPoints.put(descriptor.getName()new MethodInjectionPoint(descriptor.getName(), nullable, kind, type, method, format, editor));
531             }
532 
533             for (Field field : getAllDeclaredFields(resolveMemberClass(member))) {
534                 if (Modifier.isStatic(field.getModifiers()) || isInjectable(field)) { continue}
535                 if (!injectionPoints.containsKey(field.getName())) {
536                     boolean nullable = field.getAnnotation(Nonnull.class== null;
537                     InjectionPoint.Kind kind = resolveKind(field);
538                     Class<?> type = field.getType();
539                     String format = resolveFormat(field);
540                     Class<? extends PropertyEditor> editor = resolveEditor(field);
541                     injectionPoints.put(field.getName()new FieldInjectionPoint(field.getName(), nullable, kind, type, field, format, editor));
542                 }
543             }
544 
545             for (InjectionPoint ip : injectionPoints.values()) {
546                 ip.apply(group, memberType, member, args);
547                 argsCopy.remove(ip.name);
548             }
549 
550             /*
551             for (Map.Entry<String, Object> e : argsCopy.entrySet()) {
552                 try {
553                     setPropertyOrFieldValue(member, e.getKey(), e.getValue());
554                     LOG.warn("Property '" + e.getKey() + "' in " + memberType + " (" + resolveMemberClass(member).getName() +
555                         ") must be annotated with @" + MVCMember.class.getName() + ".");
556                 } catch (PropertyException ignored) {
557                     // OK
558                 }
559             }
560             */
561             setPropertiesOrFieldsNoException(member, argsCopy);
562         }
563     }
564 
565     @Nonnull
566     protected InjectionPoint.Kind resolveKind(@Nonnull AnnotatedElement element) {
567         if (isContextual(element)) {
568             return InjectionPoint.Kind.CONTEXTUAL;
569         else if (isMvcMember(element)) {
570             return InjectionPoint.Kind.MEMBER;
571         }
572         return InjectionPoint.Kind.OTHER;
573     }
574 
575     @Nonnull
576     protected String resolveFormat(@Nonnull AnnotatedElement element) {
577         if (isMvcMember(element)) {
578             return element.getAnnotation(MVCMember.class).format();
579         }
580         return "";
581     }
582 
583     @Nonnull
584     protected Class<? extends PropertyEditor> resolveEditor(@Nonnull AnnotatedElement element) {
585         if (isMvcMember(element)) {
586             return element.getAnnotation(MVCMember.class).editor();
587         }
588         return PropertyEditorResolver.NoopPropertyEditor.class;
589     }
590 
591     protected boolean isContextual(AnnotatedElement element) {
592         return element != null && element.getAnnotation(Contextual.class!= null;
593     }
594 
595     protected boolean isInjectable(AnnotatedElement element) {
596         return element != null && element.getAnnotation(Inject.class!= null;
597     }
598 
599     protected boolean isMvcMember(AnnotatedElement element) {
600         return element != null && element.getAnnotation(MVCMember.class!= null;
601     }
602 
603     protected void doAddGroup(@Nonnull MVCGroup group) {
604         addGroup(group);
605     }
606 
607     public void destroyMVCGroup(@Nonnull String mvcId) {
608         MVCGroup group = findGroup(mvcId);
609         LOG.debug("Group '{}' points to {}", mvcId, group);
610 
611         if (group == null) { return}
612 
613         LOG.debug("Destroying MVC group identified by '{}'", mvcId);
614 
615         if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LISTENER)) {
616             GriffonController controller = group.getController();
617             if (controller != null) {
618                 getApplication().getEventRouter().removeEventListener(controller);
619             }
620         }
621 
622         boolean fireDestructionEvents = isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_DESTRUCTION);
623 
624         destroyMembers(group, fireDestructionEvents);
625 
626         doRemoveGroup(group);
627         group.destroy();
628 
629         if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LIFECYCLE)) {
630             getApplication().getEventRouter().publishEvent(ApplicationEvent.DESTROY_MVC_GROUP.getName(), asList(group));
631         }
632     }
633 
634     protected void destroyMembers(@Nonnull MVCGroup group, boolean fireDestructionEvents) {
635         for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
636             Object member = memberEntry.getValue();
637             if (member instanceof GriffonArtifact) {
638                 destroyArtifactMember(memberEntry.getKey()(GriffonArtifactmember, fireDestructionEvents);
639             else {
640                 destroyNonArtifactMember(memberEntry.getKey(), member, fireDestructionEvents);
641             }
642 
643         }
644 
645         if (group instanceof AbstractMVCGroup) {
646             List<Object> injectedInstances = ((AbstractMVCGroupgroup).getInjectedInstances();
647             for (Object instance : injectedInstances) {
648                 getApplication().getInjector().release(instance);
649             }
650             injectedInstances.clear();
651         }
652     }
653 
654     protected void destroyArtifactMember(@Nonnull String type, @Nonnull GriffonArtifact member, boolean fireDestructionEvents) {
655         if (member instanceof GriffonMvcArtifact) {
656             final GriffonMvcArtifact artifact = (GriffonMvcArtifactmember;
657             if (fireDestructionEvents) {
658                 getApplication().getEventRouter().publishEvent(ApplicationEvent.DESTROY_INSTANCE.getName(), asList(artifact.getTypeClass(), artifact));
659             }
660 
661             if (artifact instanceof GriffonView) {
662                 getApplication().getUIThreadManager().runInsideUISync(new Runnable() {
663                     @Override
664                     public void run() {
665                         try {
666                             artifact.mvcGroupDestroy();
667                         catch (RuntimeException e) {
668                             throw (RuntimeExceptionsanitize(e);
669                         }
670                     }
671                 });
672             else {
673                 artifact.mvcGroupDestroy();
674             }
675 
676             // clear all parent* references
677             for (String parentMemberName : new String[]{"parentModel""parentView""parentController""parentGroup"}) {
678                 setPropertyOrFieldValueNoException(member, parentMemberName, null);
679             }
680         }
681 
682         destroyContextualMemberProperties(type, member);
683     }
684 
685     protected void destroyContextualMemberProperties(@Nonnull String type, @Nonnull GriffonArtifact member) {
686         for (Field field : getAllDeclaredFields(member.getTypeClass())) {
687             if (isContextual(field&& !field.getType().isPrimitive()) {
688                 try {
689                     setFieldValue(member, field.getName()null);
690                 catch (FieldException e) {
691                     throw new IllegalStateException("Could not nullify field '" +
692                         field.getName() "' in " + type + " (" + member.getTypeClass().getName() ")", e);
693                 }
694             }
695         }
696     }
697 
698     protected void destroyNonArtifactMember(@Nonnull String type, @Nonnull Object member, boolean fireDestructionEvents) {
699         // empty
700     }
701 
702     protected void doRemoveGroup(@Nonnull MVCGroup group) {
703         removeGroup(group);
704     }
705 
706     protected boolean isConfigFlagEnabled(@Nonnull MVCGroupConfiguration configuration, @Nonnull String key) {
707         return getConfigValueAsBoolean(configuration.getConfig(), key, true);
708     }
709 
710     @Nonnull
711     private static Class<?> resolveMemberClass(@Nonnull Object member) {
712         if (member instanceof GriffonArtifact) {
713             return ((GriffonArtifactmember).getTypeClass();
714         }
715         return member.getClass();
716     }
717 
718     @Nullable
719     protected Class<?> loadClass(@Nonnull String className) {
720         try {
721             return applicationClassLoader.get().loadClass(className);
722         catch (ClassNotFoundException e) {
723             // #39 do not ignore this CNFE
724             throw new GriffonException(e.toString(), e);
725         }
726     }
727 
728     protected static final class ClassHolder {
729         protected Class<?> regularClass;
730         protected Class<? extends GriffonArtifact> artifactClass;
731     }
732 }