From 3ef1b92b173e142c166c5e99e4f16928ec993ec9 Mon Sep 17 00:00:00 2001 From: Dancovich Date: Tue, 10 Sep 2013 10:50:31 -0300 Subject: [PATCH] Implementando EntityManagerProducer com escopo configurável. --- impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/context/CustomContextProducer.java | 70 ++++++++++++++++++++++++++++++++-------------------------------------- impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/ConfigurationEnumValueExtractor.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/ConfigurationLoader.java | 8 +++++++- impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/Management.java | 17 +++++++++++++++++ impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/bootstrap/EntityManagerBootstrap.java | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/configuration/EntityManagerConfig.java | 37 +++++++++++++++++++++++++++++-------- impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/context/PersistenceContext.java | 12 ------------ impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/producer/EntityManagerProducer.java | 3 --- impl/extension/jpa/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension | 1 + impl/extension/jpa/src/main/resources/demoiselle-jpa-bundle.properties | 3 ++- 10 files changed, 338 insertions(+), 63 deletions(-) create mode 100644 impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/ConfigurationEnumValueExtractor.java create mode 100644 impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/bootstrap/EntityManagerBootstrap.java delete mode 100644 impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/context/PersistenceContext.java create mode 100644 impl/extension/jpa/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension diff --git a/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/context/CustomContextProducer.java b/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/context/CustomContextProducer.java index c9f5e4f..159ee28 100644 --- a/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/context/CustomContextProducer.java +++ b/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/context/CustomContextProducer.java @@ -52,10 +52,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.AfterBeanDiscovery; import javax.enterprise.inject.spi.InjectionPoint; import org.slf4j.Logger; @@ -86,20 +86,20 @@ public class CustomContextProducer { private transient ResourceBundle bundle; - private List contexts = new ArrayList(); - - /** - * Store a context into this producer. The context must have - * been registered into CDI by a portable extension, this method will not do this. + *

Store a context into this producer. The context must have + * been registered into CDI (unsing {@link AfterBeanDiscovery#addContext(javax.enterprise.context.spi.Context context)}) by a portable extension, + * this method will not do this.

+ * + *

This producer can only produce contexts registered through this method.

* */ public void addRegisteredContext(CustomContext context){ Logger logger = getLogger(); ResourceBundle bundle = getBundle(); - if (!contexts.contains(context)){ - contexts.add(context); + if (!getContexts().contains(context)){ + getContexts().add(context); logger.debug( bundle.getString("bootstrap-context-added", context.getClass().getCanonicalName() , context.getScope().getSimpleName() ) ); } else{ @@ -108,65 +108,59 @@ public class CustomContextProducer { } /** - * Store a list of contexts into this producer. The contexts must have - * been registered into CDI by a portable extension, this method will not do this. - * - */ - @PostConstruct - public void addRegisteredContexts(){ - CustomContextBootstrap contextBootstrap = Beans.getReference(CustomContextBootstrap.class); - - List contexts = contextBootstrap.getCustomContexts(); - - for (CustomContext context : contexts){ - addRegisteredContext(context); - } - } - - /** * Deactivates all registered contexts and clear the context collection */ @PreDestroy public void closeContexts(){ //Desativa todos os contextos registrados. - for (CustomContext context : contexts){ + for (CustomContext context : getContexts()){ context.deactivate(); } - contexts.clear(); + getContexts().clear(); + } + + private List getContexts(){ + /* The demoiselle-core CustomContextBootstrap class creates default contexts for the main + * scopes of an application (request, session and conversation) and some custom contexts + * (view and static). This method injects a reference to the CustomContextBootstrap to obtain those + * contexts. Also any context registered after application start-up will be obtained by this method. */ + + CustomContextBootstrap contextBootstrap = Beans.getReference(CustomContextBootstrap.class); + return contextBootstrap.getCustomContexts(); } /////////////PRODUCERS/////////////////// @Produces - public RequestContext getRequestContext(InjectionPoint ip , CustomContextBootstrap extension){ - return getContext(ip, extension); + public RequestContext getRequestContext(InjectionPoint ip){ + return getContext(ip); } @Produces - public SessionContext getSessionContext(InjectionPoint ip , CustomContextBootstrap extension){ - return getContext(ip, extension); + public SessionContext getSessionContext(InjectionPoint ip){ + return getContext(ip); } @Produces - public ViewContext getViewContext(InjectionPoint ip , CustomContextBootstrap extension){ - return getContext(ip, extension); + public ViewContext getViewContext(InjectionPoint ip){ + return getContext(ip); } @Produces - public StaticContext getStaticContext(InjectionPoint ip , CustomContextBootstrap extension){ - return getContext(ip, extension); + public StaticContext getStaticContext(InjectionPoint ip){ + return getContext(ip); } @Produces - public ConversationContext getConversationContext(InjectionPoint ip , CustomContextBootstrap extension){ - return getContext(ip, extension); + public ConversationContext getConversationContext(InjectionPoint ip){ + return getContext(ip); } /////////////END OF PRODUCERS/////////////////// @SuppressWarnings("unchecked") - private T getContext(InjectionPoint ip , CustomContextBootstrap extension){ + private T getContext(InjectionPoint ip){ T producedContext = null; if (ip!=null){ @@ -186,7 +180,7 @@ public class CustomContextProducer { ArrayList selectableContexts = new ArrayList(); - for (CustomContext context : contexts){ + for (CustomContext context : getContexts()){ if ( contextClass.isAssignableFrom( context.getClass() ) ){ if (context.isActive()){ producedContext = context; diff --git a/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/ConfigurationEnumValueExtractor.java b/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/ConfigurationEnumValueExtractor.java new file mode 100644 index 0000000..b286498 --- /dev/null +++ b/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/ConfigurationEnumValueExtractor.java @@ -0,0 +1,82 @@ +/* + * Demoiselle Framework + * Copyright (C) 2010 SERPRO + * ---------------------------------------------------------------------------- + * This file is part of Demoiselle Framework. + * + * Demoiselle Framework is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License version 3 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this program; if not, see + * or write to the Free Software Foundation, Inc., 51 Franklin Street, + * Fifth Floor, Boston, MA 02110-1301, USA. + * ---------------------------------------------------------------------------- + * Este arquivo é parte do Framework Demoiselle. + * + * O Framework Demoiselle é um software livre; você pode redistribuí-lo e/ou + * modificá-lo dentro dos termos da GNU LGPL versão 3 como publicada pela Fundação + * do Software Livre (FSF). + * + * Este programa é distribuído na esperança que possa ser útil, mas SEM NENHUMA + * GARANTIA; sem uma garantia implícita de ADEQUAÇÃO a qualquer MERCADO ou + * APLICAÇÃO EM PARTICULAR. Veja a Licença Pública Geral GNU/LGPL em português + * para maiores detalhes. + * + * Você deve ter recebido uma cópia da GNU LGPL versão 3, sob o título + * "LICENCA.txt", junto com esse programa. Se não, acesse + * ou escreva para a Fundação do Software Livre (FSF) Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02111-1301, USA. + */ +package br.gov.frameworkdemoiselle.internal.implementation; + +import java.lang.reflect.Field; +import java.util.Locale; + +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.ConversionException; + +import br.gov.frameworkdemoiselle.annotation.Priority; +import br.gov.frameworkdemoiselle.configuration.ConfigurationValueExtractor; +import br.gov.frameworkdemoiselle.util.ResourceBundle; + +@Priority(Priority.L2_PRIORITY) +public class ConfigurationEnumValueExtractor implements ConfigurationValueExtractor{ + + private transient ResourceBundle bundle; + + @Override + public Object getValue(String prefix, String key, Field field, Configuration configuration) throws Exception { + String value = configuration.getString(prefix + key); + + Object enums[] = field.getDeclaringClass().getEnumConstants(); + + for (int i=0; i fields; public void load(Object object) throws ConfigurationException { - getLogger().debug(getBundle().getString("loading-configuration-class", object.getClass().getName())); + load(object,true); + } + + public void load(Object object,boolean logLoadingProcess) throws ConfigurationException { + if (logLoadingProcess){ + getLogger().debug(getBundle().getString("loading-configuration-class", object.getClass().getName())); + } this.object = object; diff --git a/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/Management.java b/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/Management.java index c3dd414..1d0ffd2 100644 --- a/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/Management.java +++ b/impl/core/src/main/java/br/gov/frameworkdemoiselle/internal/implementation/Management.java @@ -55,6 +55,7 @@ import org.slf4j.Logger; import br.gov.frameworkdemoiselle.annotation.ManagedProperty; import br.gov.frameworkdemoiselle.annotation.Name; +import br.gov.frameworkdemoiselle.context.ConversationContext; import br.gov.frameworkdemoiselle.context.RequestContext; import br.gov.frameworkdemoiselle.context.SessionContext; import br.gov.frameworkdemoiselle.context.ViewContext; @@ -306,6 +307,7 @@ public class Management implements Serializable { RequestContext requestContext = Beans.getReference(RequestContext.class); ViewContext viewContext = Beans.getReference(ViewContext.class); SessionContext sessionContext = Beans.getReference(SessionContext.class); + ConversationContext conversationContext = Beans.getReference(ConversationContext.class); if (!requestContext.isActive()){ logger.debug(bundle.getString("management-debug-starting-custom-context", @@ -327,12 +329,20 @@ public class Management implements Serializable { sessionContext.activate(); } + + if (!conversationContext.isActive()){ + logger.debug(bundle.getString("management-debug-starting-custom-context", + conversationContext.getClass().getCanonicalName(), managedType.getCanonicalName())); + + conversationContext.activate(); + } } private void deactivateContexts(Class managedType) { RequestContext requestContext = Beans.getReference(RequestContext.class); ViewContext viewContext = Beans.getReference(ViewContext.class); SessionContext sessionContext = Beans.getReference(SessionContext.class); + ConversationContext conversationContext = Beans.getReference(ConversationContext.class); if (requestContext.isActive()){ logger.debug(bundle.getString("management-debug-stoping-custom-context", @@ -354,6 +364,13 @@ public class Management implements Serializable { sessionContext.deactivate(); } + + if (!conversationContext.isActive()){ + logger.debug(bundle.getString("management-debug-starting-custom-context", + conversationContext.getClass().getCanonicalName(), managedType.getCanonicalName())); + + conversationContext.activate(); + } } public void shutdown(Collection> monitoringExtensions) { diff --git a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/bootstrap/EntityManagerBootstrap.java b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/bootstrap/EntityManagerBootstrap.java new file mode 100644 index 0000000..e7801bb --- /dev/null +++ b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/bootstrap/EntityManagerBootstrap.java @@ -0,0 +1,168 @@ +package br.gov.frameworkdemoiselle.internal.bootstrap; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.ConversationScoped; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.context.SessionScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AnnotatedConstructor; +import javax.enterprise.inject.spi.AnnotatedField; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.util.AnnotationLiteral; + +import br.gov.frameworkdemoiselle.annotation.ViewScoped; +import br.gov.frameworkdemoiselle.internal.configuration.EntityManagerConfig; +import br.gov.frameworkdemoiselle.internal.configuration.EntityManagerConfig.EntityManagerScope; +import br.gov.frameworkdemoiselle.internal.implementation.ConfigurationLoader; +import br.gov.frameworkdemoiselle.internal.producer.EntityManagerProducer; + + +public class EntityManagerBootstrap implements Extension { + + public void selectScopeForEntityManager(@Observes final ProcessAnnotatedType event, BeanManager beanManager) { + EntityManagerConfig config = new EntityManagerConfig(); + new ConfigurationLoader().load(config,false); + final EntityManagerScope entityManagerScope = config.getEntityManagerScope(); + + if (entityManagerScope != EntityManagerScope.NOSCOPE){ + AnnotatedType annotatedType = new AnnotatedType() { + + private AnnotatedType delegate = event.getAnnotatedType(); + + public Class getJavaClass() { + return delegate.getJavaClass(); + } + + public Type getBaseType() { + return delegate.getBaseType(); + } + + public Set> getConstructors() { + return delegate.getConstructors(); + } + + public Set getTypeClosure() { + return delegate.getTypeClosure(); + } + + public Set> getMethods() { + return delegate.getMethods(); + } + + @SuppressWarnings("unchecked") + public T getAnnotation(Class annotationType) { + T returnedAnnotation = null; + Class expectedScope; + + switch(entityManagerScope){ + case APPLICATION: + expectedScope = ApplicationScoped.class; + break; + case CONVERSATION: + expectedScope = ConversationScoped.class; + break; + case REQUEST: + expectedScope = RequestScoped.class; + break; + case SESSION: + expectedScope = SessionScoped.class; + break; + case VIEW: + expectedScope = ViewScoped.class; + break; + default: + expectedScope = null; + break; + } + + if (annotationType.equals(expectedScope)){ + switch(entityManagerScope){ + case APPLICATION: + returnedAnnotation = (T) new ApplicationScopedLiteral(); + break; + case CONVERSATION: + returnedAnnotation = (T) new ConversationScopedLiteral(); + break; + case REQUEST: + returnedAnnotation = (T) new ApplicationScopedLiteral(); + break; + case SESSION: + returnedAnnotation = (T) new SessionScopedLiteral(); + break; + case VIEW: + returnedAnnotation = (T) new ViewScopedLiteral(); + break; + default: + returnedAnnotation = delegate.getAnnotation(annotationType); + break; + } + } + else{ + returnedAnnotation = delegate.getAnnotation(annotationType); + } + + return returnedAnnotation; + } + + public Set> getFields() { + return delegate.getFields(); + } + + public Set getAnnotations() { + return delegate.getAnnotations(); + } + + public boolean isAnnotationPresent(Class annotationType) { + return delegate.isAnnotationPresent(annotationType); + } + + + }; + + event.setAnnotatedType(annotatedType); + } + } + + @SuppressWarnings("all") + class ApplicationScopedLiteral extends AnnotationLiteral implements ApplicationScoped { + private static final long serialVersionUID = 1L; + + private ApplicationScopedLiteral() {} + } + + @SuppressWarnings("all") + class RequestScopedLiteral extends AnnotationLiteral implements RequestScoped { + private static final long serialVersionUID = 1L; + + private RequestScopedLiteral(){} + } + + @SuppressWarnings("all") + class SessionScopedLiteral extends AnnotationLiteral implements SessionScoped { + private static final long serialVersionUID = 1L; + + private SessionScopedLiteral(){} + } + + @SuppressWarnings("all") + class ViewScopedLiteral extends AnnotationLiteral implements ViewScoped { + private static final long serialVersionUID = 1L; + + private ViewScopedLiteral(){} + } + + @SuppressWarnings("all") + class ConversationScopedLiteral extends AnnotationLiteral implements ConversationScoped { + private static final long serialVersionUID = 1L; + + private ConversationScopedLiteral(){} + } +} diff --git a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/configuration/EntityManagerConfig.java b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/configuration/EntityManagerConfig.java index e93ddf3..d103b0c 100644 --- a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/configuration/EntityManagerConfig.java +++ b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/configuration/EntityManagerConfig.java @@ -38,6 +38,8 @@ package br.gov.frameworkdemoiselle.internal.configuration; import java.io.Serializable; +import javax.persistence.EntityManager; + import org.slf4j.Logger; import br.gov.frameworkdemoiselle.annotation.Name; @@ -58,14 +60,15 @@ public class EntityManagerConfig implements Serializable { */ // TODO Implementação apenas para manter a compatibilidade entre a versão 2.3 com a 2.4. @Name("unit.name") + @Deprecated private String persistenceUnitName; @Name("default.unit.name") private String defaultPersistenceUnitName; @Name("entitymanager.scope") - private String entityManagerScope = "request"; - + private EntityManagerScope entityManagerScope = EntityManagerScope.REQUEST; + /** * Getter for persistence unit name. */ @@ -74,6 +77,7 @@ public class EntityManagerConfig implements Serializable { * @deprecated * @return */ + @Deprecated public String getPersistenceUnitName() { return persistenceUnitName; } @@ -96,15 +100,32 @@ public class EntityManagerConfig implements Serializable { return defaultPersistenceUnitName; } - - public String getEntityManagerScope() { + /** + *

Defines the scope of {@link EntityManager}'s produced by the internal producer.

+ * + *

Valid values are NOSCOPE,REQUEST,SESSION,VIEW,CONVERSATION and APPLICATION.

+ * + *

NOSCOPE means every injected entity manager will be a different instance.

+ * + *

The default value is REQUEST, meaning the producer will create the same + * entity manager for the duration of the request.

+ * + */ + public EntityManagerScope getEntityManagerScope() { return entityManagerScope; } - - public void setEntityManagerScope(String entityManagerScope) { + public void setEntityManagerScope(EntityManagerScope entityManagerScope) { this.entityManagerScope = entityManagerScope; } - - + + /** + * Supported scopes for the entity manager + * + * @author serpro + * + */ + public enum EntityManagerScope{ + NOSCOPE,REQUEST,SESSION,VIEW,CONVERSATION,APPLICATION; + } } diff --git a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/context/PersistenceContext.java b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/context/PersistenceContext.java deleted file mode 100644 index ae880a7..0000000 --- a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/context/PersistenceContext.java +++ /dev/null @@ -1,12 +0,0 @@ -package br.gov.frameworkdemoiselle.internal.context; - -import java.lang.annotation.Annotation; - - -public class PersistenceContext extends AbstractThreadLocalContext { - - public PersistenceContext(Class scope) { - super(scope); - } - -} diff --git a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/producer/EntityManagerProducer.java b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/producer/EntityManagerProducer.java index 2ebe282..c728180 100644 --- a/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/producer/EntityManagerProducer.java +++ b/impl/extension/jpa/src/main/java/br/gov/frameworkdemoiselle/internal/producer/EntityManagerProducer.java @@ -44,7 +44,6 @@ import java.util.Set; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Default; import javax.enterprise.inject.Produces; import javax.enterprise.inject.spi.InjectionPoint; @@ -66,9 +65,7 @@ import br.gov.frameworkdemoiselle.util.ResourceBundle; * Factory class responsible to produces instances of EntityManager. Produces instances based on informations defined in * persistence.xml, demoiselle.properties or @PersistenceUnit annotation. *

- * TODO allow users to define EntityManager's scope using demoiselle.properties */ -@RequestScoped public class EntityManagerProducer implements Serializable { private static final long serialVersionUID = 1L; diff --git a/impl/extension/jpa/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/impl/extension/jpa/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 0000000..8be97d3 --- /dev/null +++ b/impl/extension/jpa/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +br.gov.frameworkdemoiselle.internal.bootstrap.EntityManagerBootstrap \ No newline at end of file diff --git a/impl/extension/jpa/src/main/resources/demoiselle-jpa-bundle.properties b/impl/extension/jpa/src/main/resources/demoiselle-jpa-bundle.properties index 23e6dcc..96a807f 100644 --- a/impl/extension/jpa/src/main/resources/demoiselle-jpa-bundle.properties +++ b/impl/extension/jpa/src/main/resources/demoiselle-jpa-bundle.properties @@ -43,4 +43,5 @@ more-than-one-persistence-unit-defined=Existe mais de uma unidade de persist\u00 persistence-unit-name-found=Unidade de persist\u00EAncia "{0}" encontrada. entity-manager-closed=O gerenciador de entidades foi fechado. no-transaction-active=Nenhuma transa\u00E7\u00E3o est\u00E1 ativa, verifique a configura\u00E7\u00E3o "{0}" no arquivo "{1}" e defina a sua estrat\u00E9gia de transa\u00E7\u00E3o. -malformed-jpql=Consulta JPQL mal formada para pagina\u00E7\u00E3o de dados. +malformed-jpql=Consulta JPQL mal formada para pagina\u00E7\u00E3o de dados. +invalid-scope-for-entity-manager=O escopo especificado para o Entity Manager \u00E9 inv\u00E1lido. Por favor informe um dos escopos v\u00E1lidos para a propriedade frameworkdemoiselle.persistence.entitymanager.scope\: request, session, view, conversation, application -- libgit2 0.21.2