/*
* Copyright 2008-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.griffon.runtime.core.mvc;
import griffon.core.ApplicationClassLoader;
import griffon.core.ApplicationEvent;
import griffon.core.GriffonApplication;
import griffon.core.artifact.ArtifactManager;
import griffon.core.artifact.GriffonArtifact;
import griffon.core.artifact.GriffonClass;
import griffon.core.artifact.GriffonController;
import griffon.core.artifact.GriffonMvcArtifact;
import griffon.core.artifact.GriffonView;
import griffon.core.mvc.MVCGroup;
import griffon.core.mvc.MVCGroupConfiguration;
import griffon.exceptions.FieldException;
import griffon.exceptions.GriffonException;
import griffon.exceptions.GriffonViewInitializationException;
import griffon.exceptions.MVCGroupInstantiationException;
import griffon.exceptions.NewInstanceException;
import griffon.inject.Contextual;
import griffon.inject.MVCMember;
import griffon.util.CollectionUtils;
import griffon.util.Instantiator;
import org.codehaus.griffon.runtime.core.injection.InjectionUnitOfWork;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static griffon.core.GriffonExceptionHandler.sanitize;
import static griffon.util.AnnotationUtils.annotationsOfMethodParameter;
import static griffon.util.AnnotationUtils.findAnnotation;
import static griffon.util.AnnotationUtils.namesFor;
import static griffon.util.ConfigUtils.getConfigValueAsBoolean;
import static griffon.util.GriffonClassUtils.getAllDeclaredFields;
import static griffon.util.GriffonClassUtils.getPropertyDescriptors;
import static griffon.util.GriffonClassUtils.setFieldValue;
import static griffon.util.GriffonClassUtils.setPropertiesOrFieldsNoException;
import static griffon.util.GriffonClassUtils.setPropertyOrFieldValueNoException;
import static griffon.util.GriffonNameUtils.capitalize;
import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
/**
* Base implementation of the {@code MVCGroupManager} interface.
*
* @author Andres Almiray
* @since 2.0.0
*/
public class DefaultMVCGroupManager extends AbstractMVCGroupManager {
private static final Logger LOG = LoggerFactory.getLogger(DefaultMVCGroupManager.class);
private static final String CONFIG_KEY_COMPONENT = "component";
private static final String CONFIG_KEY_EVENTS_LIFECYCLE = "events.lifecycle";
private static final String CONFIG_KEY_EVENTS_INSTANTIATION = "events.instantiation";
private static final String CONFIG_KEY_EVENTS_DESTRUCTION = "events.destruction";
private static final String CONFIG_KEY_EVENTS_LISTENER = "events.listener";
private static final String KEY_PARENT_GROUP = "parentGroup";
protected final ApplicationClassLoader applicationClassLoader;
protected final Instantiator instantiator;
@Inject
public DefaultMVCGroupManager(@Nonnull GriffonApplication application, @Nonnull ApplicationClassLoader applicationClassLoader, @Nonnull Instantiator instantiator) {
super(application);
this.applicationClassLoader = requireNonNull(applicationClassLoader, "Argument 'applicationClassLoader' must not be null");
this.instantiator = requireNonNull(instantiator, "Argument 'instantiator' must not be null");
}
protected void doInitialize(@Nonnull Map<String, MVCGroupConfiguration> configurations) {
requireNonNull(configurations, "Argument 'configurations' must not be null");
for (MVCGroupConfiguration configuration : configurations.values()) {
addConfiguration(configuration);
}
}
@Nonnull
protected MVCGroup createMVCGroup(@Nonnull MVCGroupConfiguration configuration, @Nullable String mvcId, @Nonnull Map<String, Object> args) {
requireNonNull(configuration, ERROR_CONFIGURATION_NULL);
requireNonNull(args, ERROR_ARGS_NULL);
mvcId = resolveMvcId(configuration, mvcId);
checkIdIsUnique(mvcId, configuration);
LOG.debug("Building MVC group '{}' with name '{}'", configuration.getMvcType(), mvcId);
Map<String, Object> argsCopy = copyAndConfigureArguments(args, configuration, mvcId);
// figure out what the classes are
Map<String, ClassHolder> classMap = new LinkedHashMap<>();
for (Map.Entry<String, String> memberEntry : configuration.getMembers().entrySet()) {
String memberType = memberEntry.getKey();
String memberClassName = memberEntry.getValue();
selectClassesPerMember(memberType, memberClassName, classMap);
}
boolean isEventPublishingEnabled = getApplication().getEventRouter().isEventPublishingEnabled();
getApplication().getEventRouter().setEventPublishingEnabled(isConfigFlagEnabled(configuration, CONFIG_KEY_EVENTS_INSTANTIATION));
Map<String, Object> instances = new LinkedHashMap<>();
List<Object> injectedInstances = new ArrayList<>();
try {
InjectionUnitOfWork.start();
} catch (IllegalStateException ise) {
throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() + "' with id '" + mvcId + "'", configuration.getMvcType(), mvcId, ise);
}
try {
instances.putAll(instantiateMembers(classMap, argsCopy));
} finally {
getApplication().getEventRouter().setEventPublishingEnabled(isEventPublishingEnabled);
try {
injectedInstances.addAll(InjectionUnitOfWork.finish());
} catch (IllegalStateException ise) {
throw new MVCGroupInstantiationException("Can not instantiate MVC group '" + configuration.getMvcType() + "' with id '" + mvcId + "'", configuration.getMvcType(), mvcId, ise);
}
}
MVCGroup group = newMVCGroup(configuration, mvcId, instances, (MVCGroup) args.get(KEY_PARENT_GROUP));
adjustMvcArguments(group, argsCopy);
boolean fireEvents = isConfigFlagEnabled(configuration, CONFIG_KEY_EVENTS_LIFECYCLE);
if (fireEvents) {
getApplication().getEventRouter().publishEvent(ApplicationEvent.INITIALIZE_MVC_GROUP.getName(), asList(configuration, group));
}
// special case -- controllers are added as application listeners
if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LISTENER)) {
GriffonController controller = group.getController();
if (controller != null) {
getApplication().getEventRouter().addEventListener(controller);
}
}
// mutually set each other to the available fields and inject args
fillReferencedProperties(group, argsCopy);
doAddGroup(group);
initializeMembers(group, argsCopy);
if (group instanceof AbstractMVCGroup) {
((AbstractMVCGroup) group).getInjectedInstances().addAll(injectedInstances);
}
if (fireEvents) {
getApplication().getEventRouter().publishEvent(ApplicationEvent.CREATE_MVC_GROUP.getName(), asList(group));
}
return group;
}
protected void adjustMvcArguments(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
// must set it again because mvcId might have been initialized internally
args.put("mvcId", group.getMvcId());
args.put("mvcGroup", group);
args.put("application", getApplication());
}
@Nonnull
@SuppressWarnings("ConstantConditions")
protected String resolveMvcId(@Nonnull MVCGroupConfiguration configuration, @Nullable String mvcId) {
boolean component = getConfigValueAsBoolean(configuration.getConfig(), CONFIG_KEY_COMPONENT, false);
if (isBlank(mvcId)) {
if (component) {
mvcId = configuration.getMvcType() + "-" + System.nanoTime();
} else {
mvcId = configuration.getMvcType();
}
}
return mvcId;
}
@SuppressWarnings("unchecked")
protected void selectClassesPerMember(@Nonnull String memberType, @Nonnull String memberClassName, @Nonnull Map<String, ClassHolder> classMap) {
GriffonClass griffonClass = getApplication().getArtifactManager().findGriffonClass(memberClassName);
ClassHolder classHolder = new ClassHolder();
if (griffonClass != null) {
classHolder.artifactClass = (Class<? extends GriffonArtifact>) griffonClass.getClazz();
} else {
classHolder.regularClass = loadClass(memberClassName);
}
classMap.put(memberType, classHolder);
}
@Nonnull
protected Map<String, Object> copyAndConfigureArguments(@Nonnull Map<String, Object> args, @Nonnull MVCGroupConfiguration configuration, @Nonnull String mvcId) {
Map<String, Object> argsCopy = CollectionUtils.<String, Object>map()
.e("application", getApplication())
.e("mvcType", configuration.getMvcType())
.e("mvcId", mvcId)
.e("configuration", configuration);
if (args.containsKey(KEY_PARENT_GROUP)) {
if (args.get(KEY_PARENT_GROUP) instanceof MVCGroup) {
MVCGroup parentGroup = (MVCGroup) args.get(KEY_PARENT_GROUP);
for (Map.Entry<String, Object> e : parentGroup.getMembers().entrySet()) {
args.put("parent" + capitalize(e.getKey()), e.getValue());
}
}
}
argsCopy.putAll(args);
return argsCopy;
}
protected void checkIdIsUnique(@Nonnull String mvcId, @Nonnull MVCGroupConfiguration configuration) {
if (findGroup(mvcId) != null) {
String action = getApplication().getConfiguration().getAsString("griffon.mvcid.collision", "exception");
if ("warning".equalsIgnoreCase(action)) {
LOG.warn("A previous instance of MVC group '{}' with id '{}' exists. Destroying the old instance first.", configuration.getMvcType(), mvcId);
destroyMVCGroup(mvcId);
} else {
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);
}
}
}
@Nonnull
protected Map<String, Object> instantiateMembers(@Nonnull Map<String, ClassHolder> classMap, @Nonnull Map<String, Object> args) {
// instantiate the parts
Map<String, Object> instanceMap = new LinkedHashMap<>();
for (Map.Entry<String, ClassHolder> classEntry : classMap.entrySet()) {
String memberType = classEntry.getKey();
if (args.containsKey(memberType)) {
// use provided value, even if null
instanceMap.put(memberType, args.get(memberType));
} else {
// otherwise create a new value
ClassHolder classHolder = classEntry.getValue();
if (classHolder.artifactClass != null) {
Class<? extends GriffonArtifact> memberClass = classHolder.artifactClass;
ArtifactManager artifactManager = getApplication().getArtifactManager();
GriffonClass griffonClass = artifactManager.findGriffonClass(memberClass);
GriffonArtifact instance = artifactManager.newInstance(griffonClass);
instanceMap.put(memberType, instance);
args.put(memberType, instance);
} else {
Class<?> memberClass = classHolder.regularClass;
try {
Object instance = instantiator.instantiate(memberClass);
instanceMap.put(memberType, instance);
args.put(memberType, instance);
} catch (RuntimeException e) {
LOG.error("Can't create member {} with {}", memberType, memberClass);
throw new NewInstanceException(memberClass, e);
}
}
}
}
return instanceMap;
}
protected void initializeMembers(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
LOG.debug("Initializing each MVC member of group '{}'", group.getMvcId());
for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
String memberType = memberEntry.getKey();
Object member = memberEntry.getValue();
if (member instanceof GriffonArtifact) {
initializeArtifactMember(group, memberType, (GriffonArtifact) member, args);
} else {
initializeNonArtifactMember(group, memberType, member, args);
}
}
}
protected void initializeArtifactMember(@Nonnull final MVCGroup group, @Nonnull String type, @Nonnull final GriffonArtifact member, @Nonnull final Map<String, Object> args) {
if (member instanceof GriffonView) {
getApplication().getUIThreadManager().runInsideUISync(new Runnable() {
@Override
public void run() {
try {
GriffonView view = (GriffonView) member;
view.initUI();
} catch (RuntimeException e) {
throw (RuntimeException) sanitize(new GriffonViewInitializationException(group.getMvcType(), group.getMvcId(), member.getClass().getName(), e));
}
((GriffonMvcArtifact) member).mvcGroupInit(args);
}
});
} else if (member instanceof GriffonMvcArtifact) {
((GriffonMvcArtifact) member).mvcGroupInit(args);
}
}
protected void initializeNonArtifactMember(@Nonnull MVCGroup group, @Nonnull String type, @Nonnull Object member, @Nonnull Map<String, Object> args) {
// empty
}
protected static abstract class InjectionPoint {
protected final String name;
protected final boolean nullable;
protected final Type type;
protected InjectionPoint(String name, boolean nullable, Type type) {
this.name = name;
this.nullable = nullable;
this.type = type;
}
protected enum Type {
MEMBER,
CONTEXTUAL,
OTHER
}
protected abstract void apply(@Nonnull MVCGroup group, @Nonnull String memberType, @Nonnull Object instance, @Nonnull Map<String, Object> args);
}
protected static class FieldInjectionPoint extends InjectionPoint {
protected final Field field;
protected FieldInjectionPoint(String name, boolean nullable, Type type, Field field) {
super(name, nullable, type);
this.field = field;
}
@Override
protected void apply(@Nonnull MVCGroup group, @Nonnull String memberType, @Nonnull Object instance, @Nonnull Map<String, Object> args) {
String[] keys = namesFor(field);
Object argValue = args.get(name);
if (type == Type.CONTEXTUAL) {
for (String key : keys) {
if (group.getContext().containsKey(key)) {
argValue = group.getContext().get(key);
break;
}
}
}
if (argValue == null) {
if (!nullable) {
if (type == Type.CONTEXTUAL) {
throw new IllegalStateException("Could not find an instance of type " +
field.getType().getName() + " under keys '" + Arrays.toString(keys) +
"' in the context of MVCGroup[" + group.getMvcType() + ":" + group.getMvcId() +
"] to be injected on field '" + field.getName() +
"' in " + type + " (" + resolveMemberClass(instance).getName() + "). Field does not accept null values.");
} else if (type == Type.MEMBER) {
throw new IllegalStateException("Could not inject argument on field '"
+ name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
"). Field does not accept null values.");
}
}
return;
}
try {
setFieldValue(instance, name, argValue);
if (type == Type.OTHER) {
LOG.warn("Field '" + name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
") must be annotated with @" + MVCMember.class.getName() + ".");
}
} catch (FieldException e) {
throw new MVCGroupInstantiationException(group.getMvcType(), group.getMvcId(), e);
}
}
}
protected static class MethodInjectionPoint extends InjectionPoint {
protected final Method method;
protected MethodInjectionPoint(String name, boolean nullable, Type type, Method method) {
super(name, nullable, type);
this.method = method;
}
@Override
protected void apply(@Nonnull MVCGroup group, @Nonnull String memberType, @Nonnull Object instance, @Nonnull Map<String, Object> args) {
if (type == Type.CONTEXTUAL) {
String[] keys = namesFor(method);
Object argValue = args.get(name);
for (String key : keys) {
if (group.getContext().containsKey(key)) {
argValue = group.getContext().get(key);
break;
}
}
if (argValue == null && !nullable) {
throw new IllegalStateException("Could not find an instance of type " +
method.getParameterTypes()[0].getName() + " under keys '" + Arrays.toString(keys) +
"' in the context of MVCGroup[" + group.getMvcType() + ":" + group.getMvcId() +
"] to be injected on property '" + name +
"' in " + type + " (" + resolveMemberClass(instance).getName() + "). Property does not accept null values.");
}
try {
method.invoke(instance, argValue);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MVCGroupInstantiationException(group.getMvcType(), group.getMvcId(), e);
}
} else {
Object argValue = args.get(name);
if (argValue == null) {
if (!nullable) {
if (type == Type.MEMBER) {
throw new IllegalStateException("Could not inject argument on property '" +
name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
"). Property does not accept null values.");
}
}
return;
}
try {
method.invoke(instance, argValue);
if (type == Type.OTHER) {
LOG.warn("Property '" + name + "' in " + memberType + " (" + resolveMemberClass(instance).getName() +
") must be annotated with @" + MVCMember.class.getName() + ".");
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MVCGroupInstantiationException(group.getMvcType(), group.getMvcId(), e);
}
}
}
}
protected void fillReferencedProperties(@Nonnull MVCGroup group, @Nonnull Map<String, Object> args) {
for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
String memberType = memberEntry.getKey();
Object member = memberEntry.getValue();
Map<String, Object> argsCopy = new LinkedHashMap<>(args);
Map<String, Field> fields = new LinkedHashMap<>();
for (Field field : getAllDeclaredFields(resolveMemberClass(member))) {
fields.put(field.getName(), field);
}
Map<String, InjectionPoint> injectionPoints = new LinkedHashMap<>();
for (PropertyDescriptor descriptor : getPropertyDescriptors(resolveMemberClass(member))) {
Method method = descriptor.getWriteMethod();
if (method == null || isInjectable(method)) { continue; }
boolean nullable = findAnnotation(annotationsOfMethodParameter(method, 0), Nonnull.class) == null;
InjectionPoint.Type type = resolveType(method);
Field field = fields.get(descriptor.getName());
if (field != null && type == InjectionPoint.Type.OTHER) {
type = resolveType(field);
nullable = field.getAnnotation(Nonnull.class) == null;
}
injectionPoints.put(descriptor.getName(), new MethodInjectionPoint(descriptor.getName(), nullable, type, method));
}
for (Field field : getAllDeclaredFields(resolveMemberClass(member))) {
if (Modifier.isStatic(field.getModifiers()) || isInjectable(field)) { continue; }
if (!injectionPoints.containsKey(field.getName())) {
boolean nullable = field.getAnnotation(Nonnull.class) == null;
InjectionPoint.Type type = resolveType(field);
injectionPoints.put(field.getName(), new FieldInjectionPoint(field.getName(), nullable, type, field));
}
}
for (InjectionPoint ip : injectionPoints.values()) {
ip.apply(group, memberType, member, args);
argsCopy.remove(ip.name);
}
/*
for (Map.Entry<String, Object> e : argsCopy.entrySet()) {
try {
setPropertyOrFieldValue(member, e.getKey(), e.getValue());
LOG.warn("Property '" + e.getKey() + "' in " + memberType + " (" + resolveMemberClass(member).getName() +
") must be annotated with @" + MVCMember.class.getName() + ".");
} catch (PropertyException ignored) {
// OK
}
}
*/
setPropertiesOrFieldsNoException(member, argsCopy);
}
}
@Nonnull
protected InjectionPoint.Type resolveType(@Nonnull AnnotatedElement element) {
if (isContextual(element)) {
return InjectionPoint.Type.CONTEXTUAL;
} else if (isMvcMember(element)) {
return InjectionPoint.Type.MEMBER;
}
return InjectionPoint.Type.OTHER;
}
protected boolean isContextual(AnnotatedElement element) {
return element != null && element.getAnnotation(Contextual.class) != null;
}
protected boolean isInjectable(AnnotatedElement element) {
return element != null && element.getAnnotation(Inject.class) != null;
}
protected boolean isMvcMember(AnnotatedElement element) {
return element != null && element.getAnnotation(MVCMember.class) != null;
}
protected void doAddGroup(@Nonnull MVCGroup group) {
addGroup(group);
}
public void destroyMVCGroup(@Nonnull String mvcId) {
MVCGroup group = findGroup(mvcId);
LOG.debug("Group '{}' points to {}", mvcId, group);
if (group == null) { return; }
LOG.debug("Destroying MVC group identified by '{}'", mvcId);
if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LISTENER)) {
GriffonController controller = group.getController();
if (controller != null) {
getApplication().getEventRouter().removeEventListener(controller);
}
}
boolean fireDestructionEvents = isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_DESTRUCTION);
destroyMembers(group, fireDestructionEvents);
doRemoveGroup(group);
group.destroy();
if (isConfigFlagEnabled(group.getConfiguration(), CONFIG_KEY_EVENTS_LIFECYCLE)) {
getApplication().getEventRouter().publishEvent(ApplicationEvent.DESTROY_MVC_GROUP.getName(), asList(group));
}
}
protected void destroyMembers(@Nonnull MVCGroup group, boolean fireDestructionEvents) {
for (Map.Entry<String, Object> memberEntry : group.getMembers().entrySet()) {
Object member = memberEntry.getValue();
if (member instanceof GriffonArtifact) {
destroyArtifactMember(memberEntry.getKey(), (GriffonArtifact) member, fireDestructionEvents);
} else {
destroyNonArtifactMember(memberEntry.getKey(), member, fireDestructionEvents);
}
}
if (group instanceof AbstractMVCGroup) {
List<Object> injectedInstances = ((AbstractMVCGroup) group).getInjectedInstances();
for (Object instance : injectedInstances) {
getApplication().getInjector().release(instance);
}
injectedInstances.clear();
}
}
protected void destroyArtifactMember(@Nonnull String type, @Nonnull GriffonArtifact member, boolean fireDestructionEvents) {
if (member instanceof GriffonMvcArtifact) {
final GriffonMvcArtifact artifact = (GriffonMvcArtifact) member;
if (fireDestructionEvents) {
getApplication().getEventRouter().publishEvent(ApplicationEvent.DESTROY_INSTANCE.getName(), asList(artifact.getTypeClass(), artifact));
}
if (artifact instanceof GriffonView) {
getApplication().getUIThreadManager().runInsideUISync(new Runnable() {
@Override
public void run() {
try {
artifact.mvcGroupDestroy();
} catch (RuntimeException e) {
throw (RuntimeException) sanitize(e);
}
}
});
} else {
artifact.mvcGroupDestroy();
}
// clear all parent* references
for (String parentMemberName : new String[]{"parentModel", "parentView", "parentController", "parentGroup"}) {
setPropertyOrFieldValueNoException(member, parentMemberName, null);
}
}
destroyContextualMemberProperties(type, member);
}
protected void destroyContextualMemberProperties(@Nonnull String type, @Nonnull GriffonArtifact member) {
for (Field field : getAllDeclaredFields(member.getTypeClass())) {
if (isContextual(field)) {
try {
setFieldValue(member, field.getName(), null);
} catch (FieldException e) {
throw new IllegalStateException("Could not nullify field " +
field.getName() + "' in " + type + " (" + member.getTypeClass().getName() + ")", e);
}
}
}
}
protected void destroyNonArtifactMember(@Nonnull String type, @Nonnull Object member, boolean fireDestructionEvents) {
// empty
}
protected void doRemoveGroup(@Nonnull MVCGroup group) {
removeGroup(group);
}
protected boolean isConfigFlagEnabled(@Nonnull MVCGroupConfiguration configuration, @Nonnull String key) {
return getConfigValueAsBoolean(configuration.getConfig(), key, true);
}
@Nonnull
private static Class<?> resolveMemberClass(@Nonnull Object member) {
if (member instanceof GriffonArtifact) {
return ((GriffonArtifact) member).getTypeClass();
}
return member.getClass();
}
@Nullable
protected Class<?> loadClass(@Nonnull String className) {
try {
return applicationClassLoader.get().loadClass(className);
} catch (ClassNotFoundException e) {
// #39 do not ignore this CNFE
throw new GriffonException(e.toString(), e);
}
}
protected static final class ClassHolder {
protected Class<?> regularClass;
protected Class<? extends GriffonArtifact> artifactClass;
}
}