/*
* Copyright (C) 2011 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.config.rebind;
import static java.util.stream.Collectors.toCollection;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.common.client.api.annotations.LocalEvent;
import org.jboss.errai.common.client.api.annotations.NonPortable;
import org.jboss.errai.common.client.api.annotations.Portable;
import org.jboss.errai.common.client.types.TypeHandlerFactory;
import org.jboss.errai.common.metadata.ScannerSingleton;
import org.jboss.errai.common.rebind.CacheStore;
import org.jboss.errai.common.rebind.CacheUtil;
import org.jboss.errai.config.util.ClassScanner;
import org.jboss.errai.reflections.util.SimplePackageFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Mike Brock
*/
public abstract class EnvUtil {
public static class EnvironmentConfigCache implements CacheStore {
private volatile EnvironmentConfig environmentConfig;
private final Map<String, String> permanentProperties = new ConcurrentHashMap<>();
public EnvironmentConfigCache() {
clear();
}
@Override
public synchronized void clear() {
environmentConfig = newEnvironmentConfig();
environmentConfig.getFrameworkProperties().putAll(permanentProperties);
}
public synchronized EnvironmentConfig get() {
return environmentConfig;
}
public void addPermanentFrameworkProperty(final String name, final String value) {
permanentProperties.put(name, value);
environmentConfig.getFrameworkProperties().put(name, value);
}
}
public static final String CONFIG_ERRAI_SERIALIZABLE_TYPE = "errai.marshalling.serializableTypes";
public static final String CONFIG_ERRAI_NONSERIALIZABLE_TYPE = "errai.marshalling.nonserializableTypes";
public static final String CONFIG_ERRAI_MAPPING_ALIASES = "errai.marshalling.mappingAliases";
public static final String CONFIG_ERRAI_IOC_ENABLED_ALTERNATIVES = "errai.ioc.enabled.alternatives";
public static final String CONFIG_ERRAI_BINDABLE_TYPES = "errai.ui.bindableTypes";
private static volatile Boolean _isJUnitTest;
public static boolean isJUnitTest() {
if (_isJUnitTest != null) return _isJUnitTest;
for (final StackTraceElement el : new Throwable().getStackTrace()) {
if (el.getClassName().startsWith("com.google.gwt.junit.client.")
|| el.getClassName().startsWith("org.junit")) {
return _isJUnitTest = Boolean.TRUE;
}
}
return _isJUnitTest = Boolean.FALSE;
}
private static volatile Boolean _isDevMode;
public static boolean isDevMode() {
if (_isDevMode != null) return _isDevMode;
for (final StackTraceElement el : new Throwable().getStackTrace()) {
if (el.getClassName().startsWith("com.google.gwt.dev.shell.OophmSessionHandler") ||
el.getClassName().startsWith("com.google.gwt.dev.codeserver")) {
return _isDevMode = Boolean.TRUE;
}
}
return _isDevMode = Boolean.FALSE;
}
private static volatile Boolean _isProdMode;
public static boolean isProdMode() {
if (_isProdMode != null) return _isProdMode;
return _isProdMode = Boolean.valueOf(!isDevMode() && !isJUnitTest());
}
public static void recordEnvironmentState() {
isJUnitTest();
isDevMode();
isProdMode();
}
private static Logger log = LoggerFactory.getLogger(EnvUtil.class);
private static EnvironmentConfig newEnvironmentConfig() {
final Map<String, String> frameworkProps = new HashMap<>();
final Map<String, String> mappingAliases = new HashMap<>();
final Set<MetaClass> exposedClasses = new HashSet<>();
final Set<MetaClass> nonportableClasses = new HashSet<>();
final Set<String> explicitTypes = new HashSet<>();
final Set<MetaClass> portableNonExposed = new HashSet<>();
nonportableClasses.addAll(ClassScanner.getTypesAnnotatedWith(NonPortable.class));
final Set<MetaClass> exposedFromScanner = new HashSet<>(ClassScanner.getTypesAnnotatedWith(Portable.class));
addExposedInnerClasses(exposedClasses, exposedFromScanner);
exposedClasses.addAll(exposedFromScanner);
final Collection<URL> erraiAppProperties = getErraiAppProperties();
processErraiAppPropertiesUrls(frameworkProps, mappingAliases, exposedClasses, nonportableClasses, explicitTypes, erraiAppProperties);
processEnvironmentConfigExtensions(exposedClasses);
// must do this before filling in interfaces and supertypes!
exposedClasses.removeAll(nonportableClasses);
for (final MetaClass cls : exposedClasses) {
fillInInterfacesAndSuperTypes(portableNonExposed, cls);
}
return new EnvironmentConfig(mappingAliases, exposedClasses, portableNonExposed, explicitTypes, frameworkProps);
}
private static void processEnvironmentConfigExtensions(final Set<MetaClass> exposedClasses) {
final Collection<MetaClass> exts = ClassScanner.getTypesAnnotatedWith(EnvironmentConfigExtension.class, true);
for (final MetaClass cls : exts) {
try {
final Class<? extends ExposedTypesProvider> providerClass = cls.asClass().asSubclass(ExposedTypesProvider.class);
for (final MetaClass exposedType : providerClass.newInstance().provideTypesToExpose()) {
if (exposedType.isPrimitive()) {
exposedClasses.add(exposedType.asBoxed());
}
else if (exposedType.isConcrete()) {
exposedClasses.add(exposedType);
}
}
}
catch (final Throwable e) {
throw new RuntimeException("unable to load environment extension: " + cls.getFullyQualifiedName(), e);
}
}
}
private static void processErraiAppPropertiesUrls(final Map<String, String> frameworkProps, final Map<String, String> mappingAliases,
final Set<MetaClass> exposedClasses, final Set<MetaClass> nonportableClasses, final Set<String> explicitTypes,
final Collection<URL> erraiAppProperties) {
for (final URL url : erraiAppProperties) {
InputStream inputStream = null;
try {
log.debug("checking " + url.getFile() + " for configured types ...");
inputStream = url.openStream();
final ResourceBundle props = new PropertyResourceBundle(inputStream);
processErraiAppPropertiesBundle(frameworkProps, mappingAliases, exposedClasses, nonportableClasses, explicitTypes, props);
}
catch (final IOException e) {
throw new RuntimeException("error reading ErraiApp.properties", e);
}
finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (final IOException e) {
//
}
}
}
}
}
private static void processErraiAppPropertiesBundle(final Map<String, String> frameworkProps, final Map<String, String> mappingAliases,
final Set<MetaClass> exposedClasses, final Set<MetaClass> nonportableClasses, final Set<String> explicitTypes,
final ResourceBundle props) {
for (final String key : props.keySet()) {
final String value = props.getString(key);
updateFrameworkProperties(frameworkProps, key, value);
if (key.equals(CONFIG_ERRAI_SERIALIZABLE_TYPE)) {
addSerializableTypes(exposedClasses, explicitTypes, value);
}
else if (key.equals(CONFIG_ERRAI_NONSERIALIZABLE_TYPE)) {
addNonSerializableTypes(exposedClasses, nonportableClasses, value);
}
else if (key.equals(CONFIG_ERRAI_MAPPING_ALIASES)) {
addMappingAliases(mappingAliases, explicitTypes, value);
}
}
}
private static void addMappingAliases(final Map<String, String> mappingAliases, final Set<String> explicitTypes,
final String value) {
for (final String s : value.split(" ")) {
try {
final String[] mapping = s.split("->");
if (mapping.length != 2) {
throw new RuntimeException("syntax error: mapping for marshalling alias: " + s);
}
final Class<?> fromMapping = Class.forName(mapping[0].trim());
final Class<?> toMapping = Class.forName(mapping[1].trim());
mappingAliases.put(fromMapping.getName(), toMapping.getName());
explicitTypes.add(fromMapping.getName());
explicitTypes.add(toMapping.getName());
}
catch (final Exception e) {
throw new RuntimeException("could not find class defined in ErraiApp.properties for mapping: " + s, e);
}
}
}
private static void addNonSerializableTypes(final Set<MetaClass> exposedClasses, final Set<MetaClass> nonportableClasses,
final String value) {
final Set<String> patterns = new LinkedHashSet<>();
for (final String s : value.split(" ")) {
final String singleValue = s.trim();
if (singleValue.endsWith("*")) {
patterns.add(singleValue);
}
else {
try {
nonportableClasses.add(MetaClassFactory.get(singleValue));
}
catch (final Exception e) {
throw new RuntimeException("could not find class defined in ErraiApp.properties as nonserializable: "
+ s, e);
}
}
}
if (!patterns.isEmpty()) {
final SimplePackageFilter filter = new SimplePackageFilter(patterns);
MetaClassFactory
.getAllCachedClasses()
.stream()
.filter(mc -> filter.apply(mc.getFullyQualifiedName()))
.collect(toCollection(() -> exposedClasses));
}
}
private static void addSerializableTypes(final Set<MetaClass> exposedClasses, final Set<String> explicitTypes,
final String value) {
final Set<String> patterns = new LinkedHashSet<>();
for (final String s : value.split(" ")) {
final String singleValue = s.trim();
if (singleValue.endsWith("*")) {
patterns.add(singleValue);
}
else {
try {
exposedClasses.add(MetaClassFactory.get(singleValue));
explicitTypes.add(singleValue);
}
catch (final Exception e) {
throw new RuntimeException("could not find class defined in ErraiApp.properties for serialization: "
+ s, e);
}
}
}
if (!patterns.isEmpty()) {
final SimplePackageFilter filter = new SimplePackageFilter(patterns);
MetaClassFactory
.getAllCachedClasses()
.stream()
.filter(mc -> filter.apply(mc.getFullyQualifiedName()))
.collect(toCollection(() -> exposedClasses));
}
}
private static void updateFrameworkProperties(final Map<String, String> frameworkProps, final String key, final String value) {
if (frameworkProps.containsKey(key)) {
if (isListValuedProperty(key)) {
// TODO should validate that different values don't conflict
final String oldValue = frameworkProps.get(key);
final String newValue = oldValue + " " + value;
log.debug("Merging property {} = {}", key, newValue);
frameworkProps.put(key, newValue);
} else {
log.warn("The property {} has been set multiple times.", key);
frameworkProps.put(key, value);
}
} else {
frameworkProps.put(key, value);
}
}
private static boolean isListValuedProperty(final String key) {
return key.equals(CONFIG_ERRAI_IOC_ENABLED_ALTERNATIVES)
|| key.equals(CONFIG_ERRAI_BINDABLE_TYPES)
|| key.equals(CONFIG_ERRAI_SERIALIZABLE_TYPE)
|| key.equals(CONFIG_ERRAI_NONSERIALIZABLE_TYPE)
|| key.equals(CONFIG_ERRAI_MAPPING_ALIASES);
}
private static void addExposedInnerClasses(final Set<MetaClass> exposedClasses, final Set<MetaClass> exposedFromScanner) {
for (final MetaClass cls : exposedFromScanner) {
for (final MetaClass decl : cls.getDeclaredClasses()) {
if (decl.isSynthetic()) {
continue;
}
exposedClasses.add(decl);
}
}
}
public static Collection<URL> getErraiAppProperties() {
try {
final Set<URL> urlList = new HashSet<>();
for (final ClassLoader classLoader : Arrays.asList(Thread.currentThread().getContextClassLoader(),
EnvUtil.class.getClassLoader())) {
final Enumeration<URL> resources = classLoader.getResources("ErraiApp.properties");
while (resources.hasMoreElements()) {
urlList.add(resources.nextElement());
}
}
return urlList;
}
catch (final IOException e) {
throw new RuntimeException("failed to load ErraiApp.properties from classloader", e);
}
}
private static void fillInInterfacesAndSuperTypes(final Set<MetaClass> set, final MetaClass type) {
for (final MetaClass iface : type.getInterfaces()) {
set.add(iface);
fillInInterfacesAndSuperTypes(set, iface);
}
if (type.getSuperClass() != null) {
fillInInterfacesAndSuperTypes(set, type.getSuperClass());
}
}
public static void clearCache() {
CacheUtil.getCache(EnvironmentConfigCache.class).clear();
}
/**
* @return an instance of {@link EnvironmentConfig}. Do NOT retain a reference to this value. Call every time
* you need additional configuration information.
*/
public static EnvironmentConfig getEnvironmentConfig() {
return CacheUtil.getCache(EnvironmentConfigCache.class).get();
}
public static boolean isPortableType(final Class<?> cls) {
final MetaClass mc = MetaClassFactory.get(cls);
return isUserPortableType(mc) || isString(mc) || isBuiltinPortableType(cls);
}
public static boolean isPortableType(final MetaClass mc) {
return isUserPortableType(mc) || isString(mc) || isBuiltinPortableType(mc.asClass());
}
private static boolean isUserPortableType(final MetaClass mc) {
return mc.isAnnotationPresent(Portable.class) || getEnvironmentConfig().getExposedClasses().contains(mc)
|| getEnvironmentConfig().getPortableSuperTypes().contains(mc);
}
private static boolean isString(final MetaClass mc) {
return String.class.getName().equals(mc.getFullyQualifiedName());
}
private static boolean isBuiltinPortableType(final Class<?> cls) {
return TypeHandlerFactory.getHandler(cls) != null;
}
public static boolean isLocalEventType(final Class<?> cls) {
return cls.isAnnotationPresent(LocalEvent.class);
}
public static boolean isLocalEventType(final MetaClass cls) {
return cls.isAnnotationPresent(LocalEvent.class);
}
public static Set<Class<?>> getAllPortableConcreteSubtypes(final Class<?> clazz) {
final Set<Class<?>> portableSubtypes = new HashSet<>();
if (isPortableType(clazz)) {
portableSubtypes.add(clazz);
}
for (final Class<?> subType : ScannerSingleton.getOrCreateInstance().getSubTypesOf(clazz)) {
if (isPortableType(subType)) {
portableSubtypes.add(subType);
}
}
return portableSubtypes;
}
public static Set<Class<?>> getAllPortableSubtypes(final Class<?> clazz) {
final Set<Class<?>> portableSubtypes = new HashSet<>();
if (clazz.isInterface() || isPortableType(clazz)) {
portableSubtypes.add(clazz);
}
for (final Class<?> subType : ScannerSingleton.getOrCreateInstance().getSubTypesOf(clazz)) {
if (clazz.isInterface() || isPortableType(subType)) {
portableSubtypes.add(subType);
}
}
return portableSubtypes;
}
}