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