DefaultMVCGroupManager.java
001 /*
002  * Copyright 2008-2016 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package org.codehaus.griffon.runtime.core.mvc;
017 
018 import griffon.core.ApplicationClassLoader;
019 import griffon.core.ApplicationEvent;
020 import griffon.core.GriffonApplication;
021 import griffon.core.artifact.ArtifactManager;
022 import griffon.core.artifact.GriffonArtifact;
023 import griffon.core.artifact.GriffonClass;
024 import griffon.core.artifact.GriffonController;
025 import griffon.core.artifact.GriffonMvcArtifact;
026 import griffon.core.artifact.GriffonView;
027 import griffon.core.mvc.MVCGroup;
028 import griffon.core.mvc.MVCGroupConfiguration;
029 import griffon.exceptions.FieldException;
030 import griffon.exceptions.GriffonException;
031 import griffon.exceptions.MVCGroupInstantiationException;
032 import griffon.exceptions.NewInstanceException;
033 import griffon.inject.Contextual;
034 import griffon.util.CollectionUtils;
035 import org.codehaus.griffon.runtime.core.injection.InjectionUnitOfWork;
036 import org.slf4j.Logger;
037 import org.slf4j.LoggerFactory;
038 
039 import javax.annotation.Nonnull;
040 import javax.annotation.Nullable;
041 import javax.inject.Inject;
042 import java.beans.PropertyDescriptor;
043 import java.lang.reflect.Field;
044 import java.lang.reflect.InvocationTargetException;
045 import java.lang.reflect.Method;
046 import java.util.ArrayList;
047 import java.util.Arrays;
048 import java.util.LinkedHashMap;
049 import java.util.List;
050 import java.util.Map;
051 
052 import static griffon.core.GriffonExceptionHandler.sanitize;
053 import static griffon.util.AnnotationUtils.annotationsOfMethodParameter;
054 import static griffon.util.AnnotationUtils.findAnnotation;
055 import static griffon.util.AnnotationUtils.nameFor;
056 import static griffon.util.AnnotationUtils.namesFor;
057 import static griffon.util.ConfigUtils.getConfigValueAsBoolean;
058 import static griffon.util.GriffonClassUtils.getAllDeclaredFields;
059 import static griffon.util.GriffonClassUtils.getPropertyDescriptors;
060 import static griffon.util.GriffonClassUtils.setFieldValue;
061 import static griffon.util.GriffonClassUtils.setPropertiesOrFieldsNoException;
062 import static griffon.util.GriffonClassUtils.setPropertyOrFieldValueNoException;
063 import static griffon.util.GriffonNameUtils.capitalize;
064 import static griffon.util.GriffonNameUtils.isBlank;
065 import static java.util.Arrays.asList;
066 import static java.util.Objects.requireNonNull;
067 
068 /**
069  * Base implementation of the {@code MVCGroupManager} interface.
070  *
071  @author Andres Almiray
072  @since 2.0.0
073  */
074 public class DefaultMVCGroupManager extends AbstractMVCGroupManager {
075     private static final Logger LOG = LoggerFactory.getLogger(DefaultMVCGroupManager.class);
076     private static final String CONFIG_KEY_COMPONENT = "component";
077     private static final String CONFIG_KEY_EVENTS_LIFECYCLE = "events.lifecycle";
078     private static final String CONFIG_KEY_EVENTS_INSTANTIATION = "events.instantiation";
079     private static final String CONFIG_KEY_EVENTS_DESTRUCTION = "events.destruction";
080     private static final String CONFIG_KEY_EVENTS_LISTENER = "events.listener";
081     private static final String KEY_PARENT_GROUP = "parentGroup";
082 
083     private final ApplicationClassLoader applicationClassLoader;
084 
085     @Inject
086     public DefaultMVCGroupManager(@Nonnull GriffonApplication application, @Nonnull ApplicationClassLoader applicationClassLoader) {
087         super(application);
088         this.applicationClassLoader = requireNonNull(applicationClassLoader, "Argument 'applicationClassLoader' must not be null");
089     }
090 
091     protected void doInitialize(@Nonnull Map<String, MVCGroupConfiguration> configurations) {
092         requireNonNull(configurations, "Argument 'configurations' must not be null");
093         for (MVCGroupConfiguration configuration : configurations.values()) {
094             addConfiguration(configuration);
095         }
096     }
097 
098     @Nonnull
099     protected MVCGroup createMVCGroup(@Nonnull MVCGroupConfiguration configuration, @Nullable String mvcId, @Nonnull Map<String, Object> args) {
100         requireNonNull(configuration, ERROR_CONFIGURATION_NULL);
101         requireNonNull(args, ERROR_ARGS_NULL);
102 
103         mvcId = resolveMvcId(configuration, mvcId);
104         checkIdIsUnique(mvcId, configuration);
105 
106         LOG.debug("Building MVC group '{}' with name '{}'", configuration.getMvcType(), mvcId);
107         Map<String, Object> argsCopy = copyAndConfigureArguments(args, configuration, mvcId);
108 
109         // figure out what the classes are
110         Map<String, ClassHolder> classMap = new LinkedHashMap<>();
111         for (Map.Entry<String, String> memberEntry : configuration.getMembers().entrySet()) {
112             String memberType = memberEntry.getKey();
113             String memberClassName = memberEntry.getValue();
114             selectClassesPerMember(memberType, memberClassName, classMap);
115         }
116 
117         boolean isEventPublishingEnabled = getApplication().getEventRouter().isEventPublishingEnabled();
118         getApplication().getEventRouter().setEventPublishingEnabled(isConfigFlagEnabled(configuration, CONFIG_KEY_EVENTS_INSTANTIATION));
119         Map<String, Object> instances = new LinkedHashMap<>();
120         List<Object> injectedInstances = new ArrayList<>();
121 
122         try {
123             InjectionUnitOfWork.start();
124         catch (IllegalStateException ise) {
125             throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() "' with id '" + mvcId + "'", configuration.getMvcType(), mvcId, ise);
126         }
127 
128         try {
129             instances.putAll(instantiateMembers(classMap, argsCopy));
130         finally {
131             getApplication().getEventRouter().setEventPublishingEnabled(isEventPublishingEnabled);
132             try {
133                 injectedInstances.addAll(InjectionUnitOfWork.finish());
134             catch (IllegalStateException ise) {
135                 throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() "' with id '" + mvcId + "'", configuration.getMvcType(), mvcId, ise);
136             }
137         }
138 
139         MVCGroup group = newMVCGroup(configuration, mvcId, instances, (MVCGroupargs.get(KEY_PARENT_GROUP));
140         adjustMvcArguments(group, argsCopy);
141 
142         boolean fireEvents = isConfigFlagEnabled(configuration, CONFIG_KEY_EVENTS_LIFECYCLE);
143         if (fireEvents) {
144             getApplication().getEventRouter().publishEvent(ApplicationEvent.INITIALIZE_MVC_GROUP.getName(), asList(configuration, group));
145         }
146 
147         // special case -- controllers are added as application listeners
148         if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LISTENER)) {
149             GriffonController controller = group.getController();
150             if (controller != null) {
151                 getApplication().getEventRouter().addEventListener(controller);
152             }
153         }
154 
155         // mutually set each other to the available fields and inject args
156         fillReferencedProperties(group, argsCopy);
157 
158         doAddGroup(group);
159 
160         initializeMembers(group, argsCopy);
161         if (group instanceof AbstractMVCGroup) {
162             ((AbstractMVCGroupgroup).getInjectedInstances().addAll(injectedInstances);
163         }
164 
165         if (fireEvents) {
166             getApplication().getEventRouter().publishEvent(ApplicationEvent.CREATE_MVC_GROUP.getName(), asList(group));
167         }
168 
169         return group;
170     }
171 
172     protected void adjustMvcArguments(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
173         // must set it again because mvcId might have been initialized internally
174         args.put("mvcId", group.getMvcId());
175         args.put("mvcGroup", group);
176         args.put("application", getApplication());
177     }
178 
179     @Nonnull
180     @SuppressWarnings("ConstantConditions")
181     protected String resolveMvcId(@Nonnull MVCGroupConfiguration configuration, @Nullable String mvcId) {
182         boolean component = getConfigValueAsBoolean(configuration.getConfig(), CONFIG_KEY_COMPONENT, false);
183 
184         if (isBlank(mvcId)) {
185             if (component) {
186                 mvcId = configuration.getMvcType() "-" + System.nanoTime();
187             else {
188                 mvcId = configuration.getMvcType();
189             }
190         }
191         return mvcId;
192     }
193 
194     @SuppressWarnings("unchecked")
195     protected void selectClassesPerMember(@Nonnull String memberType, @Nonnull String memberClassName, @Nonnull Map<String, ClassHolder> classMap) {
196         GriffonClass griffonClass = getApplication().getArtifactManager().findGriffonClass(memberClassName);
197         ClassHolder classHolder = new ClassHolder();
198         if (griffonClass != null) {
199             classHolder.artifactClass = (Class<? extends GriffonArtifact>griffonClass.getClazz();
200         else {
201             classHolder.regularClass = loadClass(memberClassName);
202         }
203         classMap.put(memberType, classHolder);
204     }
205 
206     @Nonnull
207     protected Map<String, Object> copyAndConfigureArguments(@Nonnull Map<String, Object> args, @Nonnull MVCGroupConfiguration configuration, @Nonnull String mvcId) {
208         Map<String, Object> argsCopy = CollectionUtils.<String, Object>map()
209             .e("application", getApplication())
210             .e("mvcType", configuration.getMvcType())
211             .e("mvcId", mvcId)
212             .e("configuration", configuration);
213 
214         if (args.containsKey(KEY_PARENT_GROUP)) {
215             if (args.get(KEY_PARENT_GROUPinstanceof MVCGroup) {
216                 MVCGroup parentGroup = (MVCGroupargs.get(KEY_PARENT_GROUP);
217                 for (Map.Entry<String, Object> e : parentGroup.getMembers().entrySet()) {
218                     args.put("parent" + capitalize(e.getKey()), e.getValue());
219                 }
220             }
221         }
222 
223         argsCopy.putAll(args);
224         return argsCopy;
225     }
226 
227     protected void checkIdIsUnique(@Nonnull String mvcId, @Nonnull MVCGroupConfiguration configuration) {
228         if (findGroup(mvcId!= null) {
229             String action = getApplication().getConfiguration().getAsString("griffon.mvcid.collision""exception");
230             if ("warning".equalsIgnoreCase(action)) {
231                 LOG.warn("A previous instance of MVC group '{}' with id '{}' exists. Destroying the old instance first.", configuration.getMvcType(), mvcId);
232                 destroyMVCGroup(mvcId);
233             else {
234                 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);
235             }
236         }
237     }
238 
239     @Nonnull
240     protected Map<String, Object> instantiateMembers(@Nonnull Map<String, ClassHolder> classMap, @Nonnull Map<String, Object> args) {
241         // instantiate the parts
242         Map<String, Object> instanceMap = new LinkedHashMap<>();
243         for (Map.Entry<String, ClassHolder> classEntry : classMap.entrySet()) {
244             String memberType = classEntry.getKey();
245             if (args.containsKey(memberType)) {
246                 // use provided value, even if null
247                 instanceMap.put(memberType, args.get(memberType));
248             else {
249                 // otherwise create a new value
250                 ClassHolder classHolder = classEntry.getValue();
251                 if (classHolder.artifactClass != null) {
252                     Class<? extends GriffonArtifact> memberClass = classHolder.artifactClass;
253                     ArtifactManager artifactManager = getApplication().getArtifactManager();
254                     GriffonClass griffonClass = artifactManager.findGriffonClass(memberClass);
255                     GriffonArtifact instance = artifactManager.newInstance(griffonClass);
256                     instanceMap.put(memberType, instance);
257                     args.put(memberType, instance);
258                 else {
259                     Class<?> memberClass = classHolder.regularClass;
260                     try {
261                         Object instance = memberClass.newInstance();
262                         getApplication().getInjector().injectMembers(instance);
263                         instanceMap.put(memberType, instance);
264                         args.put(memberType, instance);
265                     catch (InstantiationException | IllegalAccessException e) {
266                         LOG.error("Can't create member {} with {}", memberType, memberClass);
267                         throw new NewInstanceException(memberClass, e);
268                     }
269                 }
270             }
271         }
272         return instanceMap;
273     }
274 
275     protected void initializeMembers(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
276         LOG.debug("Initializing each MVC member of group '{}'", group.getMvcId());
277         for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
278             String memberType = memberEntry.getKey();
279             Object member = memberEntry.getValue();
280             if (member instanceof GriffonArtifact) {
281                 initializeArtifactMember(group, memberType, (GriffonArtifactmember, args);
282             else {
283                 initializeNonArtifactMember(group, memberType, member, args);
284             }
285         }
286     }
287 
288     protected void initializeArtifactMember(@Nonnull MVCGroup group, @Nonnull String type, final @Nonnull GriffonArtifact member, final @Nonnull Map<String, Object> args) {
289         if (member instanceof GriffonView) {
290             getApplication().getUIThreadManager().runInsideUISync(new Runnable() {
291                 @Override
292                 public void run() {
293                     try {
294                         GriffonView view = (GriffonViewmember;
295                         view.initUI();
296                         view.mvcGroupInit(args);
297                     catch (RuntimeException e) {
298                         throw (RuntimeExceptionsanitize(e);
299                     }
300                 }
301             });
302         else if (member instanceof GriffonMvcArtifact) {
303             ((GriffonMvcArtifactmember).mvcGroupInit(args);
304         }
305     }
306 
307     protected void initializeNonArtifactMember(@Nonnull MVCGroup group, @Nonnull String type, @Nonnull Object member, @Nonnull Map<String, Object> args) {
308         // empty
309     }
310 
311     protected void fillReferencedProperties(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
312         for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
313             String memberType = memberEntry.getKey();
314             Object member = memberEntry.getValue();
315             if (member instanceof GriffonArtifact) {
316                 fillArtifactMemberProperties(memberType, (GriffonArtifactmember, args);
317             else {
318                 fillNonArtifactMemberProperties(memberType, member, args);
319             }
320             fillContextualMemberProperties(group, memberType, member);
321         }
322     }
323 
324     private void fillArtifactMemberProperties(@Nonnull String type, @Nonnull GriffonArtifact member, @Nonnull Map<String, Object> args) {
325         // set the args and instances
326         setPropertiesOrFieldsNoException(member, args);
327     }
328 
329     protected void fillNonArtifactMemberProperties(@Nonnull String type, @Nonnull Object member, @Nonnull Map<String, Object> args) {
330         // empty
331     }
332 
333     protected void fillContextualMemberProperties(@Nonnull MVCGroup group, @Nonnull String type, @Nonnull Object member) {
334         for (PropertyDescriptor descriptor : getPropertyDescriptors(member.getClass())) {
335             Method method = descriptor.getWriteMethod();
336             if (method != null && method.getAnnotation(Contextual.class!= null) {
337                 String key = nameFor(method);
338                 Object arg = group.getContext().get(key);
339 
340                 Nonnull nonNull = findAnnotation(annotationsOfMethodParameter(method, 0), Nonnull.class);
341                 if (arg == null && nonNull != null) {
342                     throw new IllegalStateException("Could not find an instance of type " +
343                         method.getParameterTypes()[0].getName() " under key '" + key +
344                         "' in the context of MVCGroup[" + group.getMvcType() ":" + group.getMvcId() +
345                         "] to be injected on property '" + descriptor.getName() +
346                         "' in " + type + " (" + member.getClass().getName() "). Property does not accept null values.");
347                 }
348 
349                 try {
350                     method.invoke(member, arg);
351                 catch (IllegalAccessException | InvocationTargetException e) {
352                     throw new MVCGroupInstantiationException(group.getMvcType(), group.getMvcId(), e);
353                 }
354             }
355         }
356 
357         for (Field field : getAllDeclaredFields(member.getClass())) {
358             if (field.getAnnotation(Contextual.class!= null) {
359                 Object value = null;
360                 String[] keys = namesFor(field);
361                 for (String key : keys) {
362                     if (group.getContext().containsKey(key)) {
363                         value = group.getContext().get(key);
364                     }
365                 }
366 
367                 if (value == null && field.getAnnotation(Nonnull.class!= null) {
368                     throw new IllegalStateException("Could not find an instance of type " +
369                         field.getType().getName() " under keys '" + Arrays.toString(keys+
370                         "' in the context of MVCGroup[" + group.getMvcType() ":" + group.getMvcId() +
371                         "] to be injected on field '" + field.getName() +
372                         "' in " + type + " (" + member.getClass().getName() "). Field does not accept null values.");
373                 }
374 
375                 try {
376                     setFieldValue(member, field.getName(), value);
377                 catch (FieldException e) {
378                     throw new MVCGroupInstantiationException(group.getMvcType(), group.getMvcId(), e);
379                 }
380             }
381         }
382     }
383 
384     protected void doAddGroup(@Nonnull MVCGroup group) {
385         addGroup(group);
386     }
387 
388     public void destroyMVCGroup(@Nonnull String mvcId) {
389         MVCGroup group = findGroup(mvcId);
390         LOG.debug("Group '{}' points to {}", mvcId, group);
391 
392         if (group == nullreturn;
393 
394         LOG.debug("Destroying MVC group identified by '{}'", mvcId);
395 
396         if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LISTENER)) {
397             GriffonController controller = group.getController();
398             if (controller != null) {
399                 getApplication().getEventRouter().removeEventListener(controller);
400             }
401         }
402 
403         boolean fireDestructionEvents = isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_DESTRUCTION);
404 
405         destroyMembers(group, fireDestructionEvents);
406 
407         doRemoveGroup(group);
408         group.destroy();
409 
410         if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LIFECYCLE)) {
411             getApplication().getEventRouter().publishEvent(ApplicationEvent.DESTROY_MVC_GROUP.getName(), asList(group));
412         }
413     }
414 
415     protected void destroyMembers(@Nonnull MVCGroup group, boolean fireDestructionEvents) {
416         for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
417             Object member = memberEntry.getValue();
418             if (member instanceof GriffonArtifact) {
419                 destroyArtifactMember(memberEntry.getKey()(GriffonArtifactmember, fireDestructionEvents);
420             else {
421                 destroyNonArtifactMember(memberEntry.getKey(), member, fireDestructionEvents);
422             }
423 
424         }
425 
426         if (group instanceof AbstractMVCGroup) {
427             List<Object> injectedInstances = ((AbstractMVCGroupgroup).getInjectedInstances();
428             for (Object instance : injectedInstances) {
429                 getApplication().getInjector().release(instance);
430             }
431             injectedInstances.clear();
432         }
433     }
434 
435     protected void destroyArtifactMember(@Nonnull String type, @Nonnull GriffonArtifact member, boolean fireDestructionEvents) {
436         if (member instanceof GriffonMvcArtifact) {
437             final GriffonMvcArtifact artifact = (GriffonMvcArtifactmember;
438             if (fireDestructionEvents) {
439                 getApplication().getEventRouter().publishEvent(ApplicationEvent.DESTROY_INSTANCE.getName(), asList(member.getClass(), artifact));
440             }
441 
442             if (artifact instanceof GriffonView) {
443                 getApplication().getUIThreadManager().runInsideUISync(new Runnable() {
444                     @Override
445                     public void run() {
446                         try {
447                             artifact.mvcGroupDestroy();
448                         catch (RuntimeException e) {
449                             throw (RuntimeExceptionsanitize(e);
450                         }
451                     }
452                 });
453             else {
454                 artifact.mvcGroupDestroy();
455             }
456 
457             // clear all parent* references
458             for (String parentMemberName : new String[]{"parentModel""parentView""parentController""parentGroup"}) {
459                 setPropertyOrFieldValueNoException(member, parentMemberName, null);
460             }
461         }
462 
463         destroyContextualMemberProperties(type, member);
464     }
465 
466     protected void destroyContextualMemberProperties(@Nonnull String type, @Nonnull GriffonArtifact member) {
467         for (Field field : getAllDeclaredFields(member.getClass())) {
468             if (field.getAnnotation(Contextual.class!= null) {
469                 try {
470                     setFieldValue(member, field.getName()null);
471                 catch (FieldException e) {
472                     throw new IllegalStateException("Could not nullify field " +
473                         field.getName() "' in " + type + " (" + member.getClass().getName() ")", e);
474                 }
475             }
476         }
477     }
478 
479     protected void destroyNonArtifactMember(@Nonnull String type, @Nonnull Object member, boolean fireDestructionEvents) {
480         // empty
481     }
482 
483     protected void doRemoveGroup(@Nonnull MVCGroup group) {
484         removeGroup(group);
485     }
486 
487     protected boolean isConfigFlagEnabled(@Nonnull MVCGroupConfiguration configuration, @Nonnull String key) {
488         return getConfigValueAsBoolean(configuration.getConfig(), key, true);
489     }
490 
491     @Nullable
492     protected Class<?> loadClass(@Nonnull String className) {
493         try {
494             return applicationClassLoader.get().loadClass(className);
495         catch (ClassNotFoundException e) {
496             // #39 do not ignore this CNFE
497             throw new GriffonException(e.toString(), e);
498         }
499     }
500 
501     protected static final class ClassHolder {
502         protected Class<?> regularClass;
503         protected Class<? extends GriffonArtifact> artifactClass;
504     }
505 }