/** * Copyright (C) 2016 Red Hat, Inc. and/or its affiliates. * * 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.jboss.errai.ui.nav.rebind; import static java.util.Collections.singleton; import static org.jboss.errai.codegen.Parameter.finalOf; import static org.jboss.errai.codegen.util.Stmt.castTo; import static org.jboss.errai.codegen.util.Stmt.declareFinalVariable; import static org.jboss.errai.codegen.util.Stmt.invokeStatic; import static org.jboss.errai.codegen.util.Stmt.loadVariable; import static org.jboss.errai.codegen.util.Stmt.newObject; import static org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.InjectableType.ExtensionProvided; import static org.jboss.errai.ioc.rebind.ioc.injector.api.WiringElementType.DependentBean; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.enterprise.context.Dependent; import org.jboss.errai.codegen.Statement; import org.jboss.errai.codegen.builder.ClassStructureBuilder; import org.jboss.errai.codegen.builder.ContextualStatementBuilder; import org.jboss.errai.codegen.builder.impl.ObjectBuilder; import org.jboss.errai.codegen.builder.impl.StatementBuilder; import org.jboss.errai.codegen.exception.GenerationException; import org.jboss.errai.codegen.meta.MetaClass; import org.jboss.errai.codegen.meta.MetaClassFactory; import org.jboss.errai.common.client.dom.Anchor; import org.jboss.errai.common.client.dom.Event; import org.jboss.errai.common.client.dom.EventListener; import org.jboss.errai.common.client.dom.Window; import org.jboss.errai.config.util.ClassScanner; import org.jboss.errai.ioc.client.api.IOCExtension; import org.jboss.errai.ioc.client.container.IOC; import org.jboss.errai.ioc.rebind.ioc.bootstrapper.AbstractBodyGenerator; import org.jboss.errai.ioc.rebind.ioc.bootstrapper.FactoryBodyGenerator; import org.jboss.errai.ioc.rebind.ioc.bootstrapper.IOCProcessingContext; import org.jboss.errai.ioc.rebind.ioc.extension.IOCExtensionConfigurator; import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraph; import org.jboss.errai.ioc.rebind.ioc.graph.api.Injectable; import org.jboss.errai.ioc.rebind.ioc.graph.api.InjectionSite; import org.jboss.errai.ioc.rebind.ioc.graph.api.QualifierFactory; import org.jboss.errai.ioc.rebind.ioc.graph.impl.DefaultCustomFactoryInjectable; import org.jboss.errai.ioc.rebind.ioc.graph.impl.FactoryNameGenerator; import org.jboss.errai.ioc.rebind.ioc.graph.impl.InjectableHandle; import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectionContext; import org.jboss.errai.ui.nav.client.local.Navigation; import org.jboss.errai.ui.nav.client.local.Page; import org.jboss.errai.ui.nav.client.local.UniquePageRole; import org.jboss.errai.ui.nav.client.local.api.TransitionTo; import org.jboss.errai.ui.nav.client.local.api.TransitionToRole; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.gwt.core.ext.GeneratorContext; /** * Allows injecting {@link Anchor} elements that transition to a {@link Page} by direct type or by * {@link UniquePageRole} with the qualifiers {@link TransitionTo} and {@link TransitionToRole}, respectively. * * @author Max Barkley <mbarkley@redhat.com> */ @IOCExtension public class TransitionProviderIOCExtension implements IOCExtensionConfigurator { private static final TransitionTo TRANSITION_TO = new TransitionTo() { @Override public Class<? extends Annotation> annotationType() { return TransitionTo.class; } @Override public Class<?> value() { return Void.class; } }; private static final TransitionToRole TRANSITION_TO_ROLE = new TransitionToRole() { @Override public Class<? extends Annotation> annotationType() { return TransitionToRole.class; } @Override public Class<? extends UniquePageRole> value() { return UniquePageRole.class; } }; private final Map<Class<?>, DefaultCustomFactoryInjectable> injectables = new HashMap<>(); private final Multimap<Class<? extends UniquePageRole>, MetaClass> pagesByRole = HashMultimap.create(); @Override public void configure(final IOCProcessingContext context, final InjectionContext injectionContext) { } @Override public void afterInitialization(final IOCProcessingContext context, final InjectionContext injectionContext) { final QualifierFactory qualifierFactory = injectionContext.getQualifierFactory(); final InjectableHandle transitionToHandle = new InjectableHandle(MetaClassFactory.get(Anchor.class), qualifierFactory.forSource(() -> new Annotation[] { TRANSITION_TO })); final InjectableHandle transitionToRoleHandle = new InjectableHandle(MetaClassFactory.get(Anchor.class), qualifierFactory.forSource(() -> new Annotation[] { TRANSITION_TO_ROLE })); scanForUniquePageRoles(context.getGeneratorContext()); registerProvider(injectionContext, transitionToHandle); registerProvider(injectionContext, transitionToRoleHandle); } private void scanForUniquePageRoles(final GeneratorContext generatorContext) { final Collection<MetaClass> pages = ClassScanner.getTypesAnnotatedWith(Page.class, generatorContext); pages .stream() .filter(type -> type.getAnnotation(Page.class).role().length > 0) .forEach(type -> { final Page anno = type.getAnnotation(Page.class); Arrays .stream(anno.role()) .filter(role -> UniquePageRole.class.isAssignableFrom(role)) .map(role -> role.<UniquePageRole>asSubclass(UniquePageRole.class)) .forEach(role -> pagesByRole.put(role, type)); }); } private void registerProvider(final InjectionContext injectionContext, final InjectableHandle transitionToHandle) { injectionContext.registerExactTypeInjectableProvider(transitionToHandle, (injectionSite, nameGenerator) -> getOrCreateInjectable(transitionToHandle, injectionSite, nameGenerator, injectionContext) ); } private DefaultCustomFactoryInjectable getOrCreateInjectable(final InjectableHandle handle, final InjectionSite injectionSite, final FactoryNameGenerator nameGenerator, final InjectionContext injectionContext) { final Class<?> targetType = assertTargetType(injectionSite, injectionContext); DefaultCustomFactoryInjectable injectable = injectables.get(targetType); if (injectable == null) { injectable = new DefaultCustomFactoryInjectable(handle, nameGenerator.generateFor(handle, ExtensionProvided), Dependent.class, singleton(DependentBean), createGenerator(targetType)); injectables.put(targetType, injectable); } return injectable; } private Class<?> assertTargetType(final InjectionSite injectionSite, final InjectionContext injectionContext) { if (injectionSite.isAnnotationPresent(TransitionTo.class)) { return assertIsPage(injectionSite); } else if (injectionSite.isAnnotationPresent(TransitionToRole.class)) { return assertRoleExistsAndIsValid(injectionSite, injectionContext); } else { throw new IllegalStateException( String.format("This extension provider should only be called for anchors with %s or %s.", TransitionTo.class.getSimpleName(), TransitionToRole.class.getSimpleName())); } } private Class<? extends UniquePageRole> assertRoleExistsAndIsValid(final InjectionSite injectionSite, final InjectionContext injectionContext) { final Class<? extends UniquePageRole> candidateRole = injectionSite.getAnnotation(TransitionToRole.class).value(); if (pagesByRole.get(candidateRole).size() == 1) { return candidateRole; } else if (pagesByRole.get(candidateRole).isEmpty()) { throw new GenerationException( String.format("An @%s Anchor was found for the role %s but no @%s exists with that role.", TransitionToRole.class.getSimpleName(), candidateRole.getName(), Page.class.getSimpleName())); } else { throw new GenerationException( String.format("An @%s Anchor was found for the role %s but multiple @%ss pages exist with that role: %s", TransitionToRole.class.getSimpleName(), candidateRole.getName(), Page.class.getSimpleName(), pagesByRole.get(candidateRole).toString())); } } private Class<?> assertIsPage(final InjectionSite injectionSite) { final Class<?> candidate = injectionSite.getAnnotation(TransitionTo.class).value(); if (!candidate.isAnnotationPresent(Page.class)) { throw new IllegalArgumentException(String.format("They type %s is not a valid value for @%s. A @%s is required.", candidate.getName(), TransitionTo.class.getSimpleName(), Page.class.getSimpleName())); } else { return candidate; } } private FactoryBodyGenerator createGenerator(final Class<?> targetType) { return new AbstractBodyGenerator() { @Override protected List<Statement> generateCreateInstanceStatements(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable, final DependencyGraph graph, final InjectionContext injectionContext) { final StatementBuilder anchorDeclaration = declareFinalVariable( "anchor", Anchor.class, castTo(Anchor.class, invokeStatic(Window.class, "getDocument") .invoke("createElement", "a"))); final ObjectBuilder clickListener = newObject(EventListener.class) .extend() .publicOverridesMethod("call", finalOf(Event.class, "event")) .append(navigationGoToInvocation(targetType)) .finish() .finish(); final ContextualStatementBuilder setClickListener = loadVariable("anchor").invoke("setOnclick", clickListener); final Statement returnValue = loadVariable("anchor").returnValue(); return Arrays.asList( anchorDeclaration, setClickListener, returnValue ); } private ContextualStatementBuilder navigationGoToInvocation(final Class<?> targetType) { final String methodName; final Object[] args; final ContextualStatementBuilder getBeanManager = invokeStatic(IOC.class, "getBeanManager"); final ContextualStatementBuilder getNavigation = getBeanManager .invoke("lookupBean", Navigation.class) .invoke("getInstance"); if (UniquePageRole.class.isAssignableFrom(targetType)) { methodName = "goToWithRole"; args = new Object[] { targetType }; } else { methodName = "goTo"; args = new Object[] { targetType, castTo(Multimap.class, invokeStatic(ImmutableMultimap.class, "of")) }; } return castTo(Navigation.class, getNavigation) .invoke(methodName, args); } }; } }