Commit 5f248157 by Shwetha GS

ATLAS-683 Refactor local type-system cache with cache provider interface (vmadugun via shwethags)

parent 440bd2ae
......@@ -60,7 +60,9 @@ distro/hbase/*.tar.gz
#solr package downloaded
distro/solr/*.tgz
# Scala-IDE specific
.cache-main
.cache-tests
# emacs files
*#
......
......@@ -92,13 +92,22 @@ public final class ApplicationProperties extends PropertiesConfiguration {
return inConf.subset(prefix);
}
public static Class getClass(String propertyName, String defaultValue) {
public static Class getClass(String propertyName, String defaultValue, Class assignableClass)
throws AtlasException {
try {
Configuration configuration = get();
String propertyValue = configuration.getString(propertyName, defaultValue);
return Class.forName(propertyValue);
Class<?> clazz = Class.forName(propertyValue);
if (assignableClass == null || assignableClass.isAssignableFrom(clazz)) {
return clazz;
} else {
String message = "Class " + clazz.getName() + " specified in property " + propertyName
+ " is not assignable to class " + assignableClass.getName();
LOG.error(message);
throw new AtlasException(message);
}
} catch (Exception e) {
throw new RuntimeException(e);
throw new AtlasException(e);
}
}
}
......@@ -122,3 +122,8 @@ atlas.login.credentials.file=${sys:atlas.home}/conf/users-credentials.properties
#########POLICY FILE PATH #########
atlas.auth.policy.file=${sys:atlas.home}/conf/policy-store.txt
######### Type Cache Provider Implementation ########
# A type cache provider class which implements
# org.apache.atlas.typesystem.types.cache.ITypeCacheProvider.
# The default is DefaultTypeCacheProvider which is a local in-memory type cache.
#atlas.typesystem.cache.provider=
......@@ -21,6 +21,7 @@ ATLAS-409 Atlas will not import avro tables with schema read from a file (dosset
ATLAS-379 Create sqoop and falcon metadata addons (venkatnrangan,bvellanki,sowmyaramesh via shwethags)
ALL CHANGES:
ATLAS-683 Refactor local type-system cache with cache provider interface (vmadugun via shwethags)
ATLAS-802 New look UI to show Business Catalog functionalities (kevalbhatt18 via yhemanth)
ATLAS-658 Improve Lineage with Backbone porting (kevalbhatt18 via yhemanth)
ATLAS-491 Business Catalog / Taxonomy (jspeidel via yhemanth)
......
......@@ -111,7 +111,11 @@ public class RepositoryMetadataModule extends com.google.inject.AbstractModule {
private static final String DELETE_HANDLER_IMPLEMENTATION_PROPERTY = "atlas.DeleteHandler.impl";
private Class<? extends DeleteHandler> getDeleteHandler() {
return ApplicationProperties.getClass(DELETE_HANDLER_IMPLEMENTATION_PROPERTY,
SoftDeleteHandler.class.getName());
try {
return ApplicationProperties.getClass(DELETE_HANDLER_IMPLEMENTATION_PROPERTY,
SoftDeleteHandler.class.getName(), DeleteHandler.class);
} catch (AtlasException e) {
throw new RuntimeException(e);
}
}
}
......@@ -18,19 +18,6 @@
package org.apache.atlas.typesystem.types;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import org.apache.atlas.AtlasException;
import org.apache.atlas.classification.InterfaceAudience;
import org.apache.atlas.typesystem.TypesDef;
import org.apache.atlas.typesystem.exception.TypeExistsException;
import org.apache.atlas.typesystem.exception.TypeNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Singleton;
import java.lang.reflect.Constructor;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
......@@ -41,10 +28,27 @@ import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Singleton;
import org.apache.atlas.ApplicationProperties;
import org.apache.atlas.AtlasException;
import org.apache.atlas.classification.InterfaceAudience;
import org.apache.atlas.typesystem.TypesDef;
import org.apache.atlas.typesystem.exception.TypeExistsException;
import org.apache.atlas.typesystem.exception.TypeNotFoundException;
import org.apache.atlas.typesystem.types.cache.DefaultTypeCacheProvider;
import org.apache.atlas.typesystem.types.cache.ITypeCacheProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@Singleton
@InterfaceAudience.Private
public class TypeSystem {
private static final Logger LOG = LoggerFactory.getLogger(TypeSystem.class);
private static final String CACHE_PROVIDER_CLASS_PROPERTY = "atlas.typesystem.cache.provider";
private static final TypeSystem INSTANCE = new TypeSystem();
private static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
......@@ -56,15 +60,9 @@ public class TypeSystem {
}
};
private Map<String, IDataType> types;
private ITypeCacheProvider typeCache;
private IdType idType;
/**
* An in-memory copy of type categories vs types for convenience.
*/
private Multimap<DataTypes.TypeCategory, String> typeCategoriesToTypeNamesMap;
private ImmutableList<String> coreTypes;
private Map<String, IDataType> coreTypes;
public TypeSystem() {
initialize();
......@@ -78,69 +76,111 @@ public class TypeSystem {
* This is only used for testing purposes. Not intended for public use.
*/
@InterfaceAudience.Private
public void reset() {
public TypeSystem reset() {
typeCache.clear(); // clear all entries in cache
initialize();
return this;
}
private void initialize() {
types = new ConcurrentHashMap<>();
typeCategoriesToTypeNamesMap = ArrayListMultimap.create(DataTypes.TypeCategory.values().length, 10);
initCacheProvider();
coreTypes = new ConcurrentHashMap<>();
registerPrimitiveTypes();
registerCoreTypes();
coreTypes = ImmutableList.copyOf(types.keySet());
}
/**
* Ideally a cache provider should have been injected in the TypeSystemProvider,
* but a singleton of TypeSystem is constructed privately within the class so that
* clients of TypeSystem would never instantiate a TypeSystem object directly in
* their code. As soon as a client makes a call to TypeSystem.getInstance(), they
* should have the singleton ready for consumption. To enable such an access pattern,
* it kind of becomes imperative to initialize the cache provider within the
* TypeSystem constructor (bypassing the GUICE way of injecting a cache provider)
*/
private void initCacheProvider() {
// read the pluggable cache provider from Atlas configuration
final String defaultCacheProvider = DefaultTypeCacheProvider.class.getName();
Class cacheProviderClass;
try {
cacheProviderClass = ApplicationProperties.getClass(CACHE_PROVIDER_CLASS_PROPERTY,
defaultCacheProvider, ITypeCacheProvider.class);
} catch (AtlasException e) {
throw new RuntimeException("Error getting type cache provider implementation class", e);
}
try {
typeCache = (ITypeCacheProvider)cacheProviderClass.newInstance();
}
catch (Exception e) {
throw new RuntimeException("Error creating instance of type cache provider implementation class " + cacheProviderClass.getName(), e);
}
}
public ImmutableList<String> getCoreTypes() {
return coreTypes;
return ImmutableList.copyOf(coreTypes.keySet());
}
public ImmutableList<String> getTypeNames() {
List<String> typeNames = new ArrayList<>(types.keySet());
typeNames.removeAll(getCoreTypes());
public ImmutableList<String> getTypeNames() throws AtlasException {
List<String> typeNames = new ArrayList<>(typeCache.getAllTypeNames());
return ImmutableList.copyOf(typeNames);
}
public ImmutableList<String> getTypeNamesByCategory(DataTypes.TypeCategory typeCategory) {
return ImmutableList.copyOf(typeCategoriesToTypeNamesMap.get(typeCategory));
public ImmutableList<String> getTypeNamesByCategory(DataTypes.TypeCategory typeCategory) throws AtlasException {
return ImmutableList.copyOf(typeCache.getTypeNames(typeCategory));
}
private void registerPrimitiveTypes() {
types.put(DataTypes.BOOLEAN_TYPE.getName(), DataTypes.BOOLEAN_TYPE);
types.put(DataTypes.BYTE_TYPE.getName(), DataTypes.BYTE_TYPE);
types.put(DataTypes.SHORT_TYPE.getName(), DataTypes.SHORT_TYPE);
types.put(DataTypes.INT_TYPE.getName(), DataTypes.INT_TYPE);
types.put(DataTypes.LONG_TYPE.getName(), DataTypes.LONG_TYPE);
types.put(DataTypes.FLOAT_TYPE.getName(), DataTypes.FLOAT_TYPE);
types.put(DataTypes.DOUBLE_TYPE.getName(), DataTypes.DOUBLE_TYPE);
types.put(DataTypes.BIGINTEGER_TYPE.getName(), DataTypes.BIGINTEGER_TYPE);
types.put(DataTypes.BIGDECIMAL_TYPE.getName(), DataTypes.BIGDECIMAL_TYPE);
types.put(DataTypes.DATE_TYPE.getName(), DataTypes.DATE_TYPE);
types.put(DataTypes.STRING_TYPE.getName(), DataTypes.STRING_TYPE);
typeCategoriesToTypeNamesMap.putAll(DataTypes.TypeCategory.PRIMITIVE, types.keySet());
coreTypes.put(DataTypes.BOOLEAN_TYPE.getName(), DataTypes.BOOLEAN_TYPE);
coreTypes.put(DataTypes.BYTE_TYPE.getName(), DataTypes.BYTE_TYPE);
coreTypes.put(DataTypes.SHORT_TYPE.getName(), DataTypes.SHORT_TYPE);
coreTypes.put(DataTypes.INT_TYPE.getName(), DataTypes.INT_TYPE);
coreTypes.put(DataTypes.LONG_TYPE.getName(), DataTypes.LONG_TYPE);
coreTypes.put(DataTypes.FLOAT_TYPE.getName(), DataTypes.FLOAT_TYPE);
coreTypes.put(DataTypes.DOUBLE_TYPE.getName(), DataTypes.DOUBLE_TYPE);
coreTypes.put(DataTypes.BIGINTEGER_TYPE.getName(), DataTypes.BIGINTEGER_TYPE);
coreTypes.put(DataTypes.BIGDECIMAL_TYPE.getName(), DataTypes.BIGDECIMAL_TYPE);
coreTypes.put(DataTypes.DATE_TYPE.getName(), DataTypes.DATE_TYPE);
coreTypes.put(DataTypes.STRING_TYPE.getName(), DataTypes.STRING_TYPE);
}
/*
* The only core OOB type we will define is the Struct to represent the Identity of an Instance.
*/
private void registerCoreTypes() {
idType = new IdType();
coreTypes.put(idType.getStructType().getName(), idType.getStructType());
}
public IdType getIdType() {
return idType;
}
public boolean isRegistered(String typeName) {
return types.containsKey(typeName);
public boolean isRegistered(String typeName) throws AtlasException {
return isCoreType(typeName) || typeCache.has(typeName);
}
protected boolean isCoreType(String typeName) {
return coreTypes.containsKey(typeName);
}
public <T> T getDataType(Class<T> cls, String name) throws AtlasException {
if (types.containsKey(name)) {
if (isCoreType(name)) {
return cls.cast(coreTypes.get(name));
}
if (typeCache.has(name)) {
try {
return cls.cast(types.get(name));
return cls.cast(typeCache.get(name));
} catch (ClassCastException cce) {
throw new AtlasException(cce);
}
......@@ -285,13 +325,12 @@ public class TypeSystem {
public EnumType defineEnumType(EnumTypeDefinition eDef) throws AtlasException {
assert eDef.name != null;
if (types.containsKey(eDef.name)) {
if (isRegistered(eDef.name)) {
throw new AtlasException(String.format("Redefinition of type %s not supported", eDef.name));
}
EnumType eT = new EnumType(this, eDef.name, eDef.description, eDef.enumValues);
types.put(eDef.name, eT);
typeCategoriesToTypeNamesMap.put(DataTypes.TypeCategory.ENUM, eDef.name);
typeCache.put(eT);
return eT;
}
......@@ -329,17 +368,14 @@ public class TypeSystem {
*
* This step should be called only after the types have been committed to the backend stores successfully.
* @param typesAdded newly added types.
* @throws AtlasException
*/
public void commitTypes(Map<String, IDataType> typesAdded) {
public void commitTypes(Map<String, IDataType> typesAdded) throws AtlasException {
for (Map.Entry<String, IDataType> typeEntry : typesAdded.entrySet()) {
String typeName = typeEntry.getKey();
IDataType type = typeEntry.getValue();
//Add/replace the new type in the typesystem
types.put(typeName, type);
// ArrayListMultiMap allows duplicates - we want to avoid this during re-activation.
if (!typeCategoriesToTypeNamesMap.containsEntry(type.getTypeCategory(), typeName)) {
typeCategoriesToTypeNamesMap.put(type.getTypeCategory(), typeName);
}
typeCache.put(type);
}
}
......@@ -372,10 +408,12 @@ public class TypeSystem {
}
private IDataType dataType(String name) throws AtlasException {
if (transientTypes.containsKey(name)) {
return transientTypes.get(name);
}
return TypeSystem.this.types.get(name);
return TypeSystem.this.getDataType(IDataType.class, name);
}
/*
......@@ -386,7 +424,7 @@ public class TypeSystem {
private void validateAndSetupShallowTypes(boolean update) throws AtlasException {
for (EnumTypeDefinition eDef : enumDefs) {
assert eDef.name != null;
if (!update && (transientTypes.containsKey(eDef.name) || types.containsKey(eDef.name))) {
if (!update && (transientTypes.containsKey(eDef.name) || isRegistered(eDef.name))) {
throw new AtlasException(String.format("Redefinition of type %s not supported", eDef.name));
}
......@@ -396,7 +434,7 @@ public class TypeSystem {
for (StructTypeDefinition sDef : structDefs) {
assert sDef.typeName != null;
if (!update && (transientTypes.containsKey(sDef.typeName) || types.containsKey(sDef.typeName))) {
if (!update && (transientTypes.containsKey(sDef.typeName) || isRegistered(sDef.typeName))) {
throw new TypeExistsException(String.format("Cannot redefine type %s", sDef.typeName));
}
StructType sT = new StructType(this, sDef.typeName, sDef.typeDescription, sDef.attributeDefinitions.length);
......@@ -407,7 +445,7 @@ public class TypeSystem {
for (HierarchicalTypeDefinition<TraitType> traitDef : traitDefs) {
assert traitDef.typeName != null;
if (!update &&
(transientTypes.containsKey(traitDef.typeName) || types.containsKey(traitDef.typeName))) {
(transientTypes.containsKey(traitDef.typeName) || isRegistered(traitDef.typeName))) {
throw new TypeExistsException(String.format("Cannot redefine type %s", traitDef.typeName));
}
TraitType tT = new TraitType(this, traitDef.typeName, traitDef.typeDescription, traitDef.superTypes,
......@@ -419,7 +457,7 @@ public class TypeSystem {
for (HierarchicalTypeDefinition<ClassType> classDef : classDefs) {
assert classDef.typeName != null;
if (!update &&
(transientTypes.containsKey(classDef.typeName) || types.containsKey(classDef.typeName))) {
(transientTypes.containsKey(classDef.typeName) || isRegistered(classDef.typeName))) {
throw new TypeExistsException(String.format("Cannot redefine type %s", classDef.typeName));
}
......@@ -582,11 +620,11 @@ public class TypeSystem {
* Step 5:
* - Validate that the update can be done
*/
private void validateUpdateIsPossible() throws TypeUpdateException {
private void validateUpdateIsPossible() throws TypeUpdateException, AtlasException {
//If the type is modified, validate that update can be done
for (IDataType newType : transientTypes.values()) {
if (TypeSystem.this.types.containsKey(newType.getName())) {
IDataType oldType = TypeSystem.this.types.get(newType.getName());
if (TypeSystem.this.isRegistered(newType.getName())) {
IDataType oldType = TypeSystem.this.typeCache.get(newType.getName());
oldType.validateUpdate(newType);
}
}
......@@ -600,7 +638,7 @@ public class TypeSystem {
}
@Override
public ImmutableList<String> getTypeNames() {
public ImmutableList<String> getTypeNames() throws AtlasException {
Set<String> typeNames = transientTypes.keySet();
typeNames.addAll(TypeSystem.this.getTypeNames());
return ImmutableList.copyOf(typeNames);
......@@ -644,17 +682,17 @@ public class TypeSystem {
@Override
public StructType defineStructType(String name, boolean errorIfExists, AttributeDefinition... attrDefs)
throws AtlasException {
throw new AtlasException("Internal Error: define type called on TrasientTypeSystem");
throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
}
@Override
public TraitType defineTraitType(HierarchicalTypeDefinition traitDef) throws AtlasException {
throw new AtlasException("Internal Error: define type called on TrasientTypeSystem");
throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
}
@Override
public ClassType defineClassType(HierarchicalTypeDefinition<ClassType> classDef) throws AtlasException {
throw new AtlasException("Internal Error: define type called on TrasientTypeSystem");
throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
}
@Override
......@@ -662,7 +700,7 @@ public class TypeSystem {
ImmutableList<StructTypeDefinition> structDefs,
ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs,
ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs) throws AtlasException {
throw new AtlasException("Internal Error: define type called on TrasientTypeSystem");
throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
}
@Override
......@@ -686,13 +724,22 @@ public class TypeSystem {
}
@Override
public void commitTypes(Map<String, IDataType> typesAdded) {
public void commitTypes(Map<String, IDataType> typesAdded) throws AtlasException {
TypeSystem.this.commitTypes(typesAdded);
}
public Map<String, IDataType> getTypesAdded() {
return new HashMap<>(transientTypes);
}
/**
* The core types do not change and they are registered
* once in the main type system.
*/
@Override
public ImmutableList<String> getCoreTypes() {
return TypeSystem.this.getCoreTypes();
}
}
public class IdType {
......@@ -700,6 +747,8 @@ public class TypeSystem {
private static final String TYPENAME_ATTRNAME = "typeName";
private static final String TYP_NAME = "__IdType";
private StructType type;
private IdType() {
AttributeDefinition idAttr =
new AttributeDefinition(ID_ATTRNAME, DataTypes.STRING_TYPE.getName(), Multiplicity.REQUIRED, false,
......@@ -712,16 +761,14 @@ public class TypeSystem {
infos[0] = new AttributeInfo(TypeSystem.this, idAttr, null);
infos[1] = new AttributeInfo(TypeSystem.this, typNmAttr, null);
StructType type = new StructType(TypeSystem.this, TYP_NAME, null, infos);
TypeSystem.this.types.put(TYP_NAME, type);
type = new StructType(TypeSystem.this, TYP_NAME, null, infos);
} catch (AtlasException me) {
throw new RuntimeException(me);
}
}
public StructType getStructType() throws AtlasException {
return getDataType(StructType.class, TYP_NAME);
public StructType getStructType() {
return type;
}
public String getName() {
......@@ -738,4 +785,5 @@ public class TypeSystem {
}
public static final String ID_STRUCT_ID_ATTRNAME = IdType.ID_ATTRNAME;
public static final String ID_STRUCT_TYP_NAME = IdType.TYP_NAME;
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.atlas.typesystem.types.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.DataTypes.TypeCategory;
import org.apache.atlas.typesystem.types.EnumType;
import org.apache.atlas.typesystem.types.IDataType;
import org.apache.atlas.typesystem.types.StructType;
import org.apache.atlas.typesystem.types.TraitType;
import com.google.inject.Singleton;
/**
* Caches the types in-memory within the same process space.
*/
@Singleton
@SuppressWarnings("rawtypes")
public class DefaultTypeCacheProvider implements ITypeCacheProvider {
private Map<String, IDataType> types_ = new ConcurrentHashMap<>();
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#has(java.lang
* .String)
*/
@Override
public boolean has(String typeName) throws AtlasException {
return types_.containsKey(typeName);
}
/* (non-Javadoc)
* @see org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#has(org.
* apache.atlas.typesystem.types.DataTypes.TypeCategory, java.lang.String)
*/
@Override
public boolean has(TypeCategory typeCategory, String typeName)
throws AtlasException {
assertValidTypeCategory(typeCategory);
return has(typeName);
}
private void assertValidTypeCategory(TypeCategory typeCategory) throws
AtlasException {
// there might no need of 'typeCategory' in this implementation for
// certain API, but for a distributed cache, it might help for the
// implementers to partition the types per their category
// while persisting so that look can be efficient
if (typeCategory == null) {
throw new AtlasException("Category of the types to be filtered is null.");
}
boolean validTypeCategory = typeCategory.equals(TypeCategory.CLASS) ||
typeCategory.equals(TypeCategory.TRAIT) ||
typeCategory.equals(TypeCategory.ENUM) ||
typeCategory.equals(TypeCategory.STRUCT);
if (!validTypeCategory) {
throw new AtlasException("Category of the types should be one of CLASS "
+ "| TRAIT | ENUM | STRUCT.");
}
}
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#get(java.lang
* .String)
*/
@Override
public IDataType get(String typeName) throws AtlasException {
return types_.get(typeName);
}
/* (non-Javadoc)
* @see org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#get(org.apache.
* atlas.typesystem.types.DataTypes.TypeCategory, java.lang.String)
*/
@Override
public IDataType get(TypeCategory typeCategory, String typeName) throws AtlasException {
assertValidTypeCategory(typeCategory);
return get(typeName);
}
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#getNames(org
* .apache.atlas.typesystem.types.DataTypes.TypeCategory)
*/
@Override
public Collection<String> getTypeNames(TypeCategory typeCategory) throws AtlasException {
assertValidTypeCategory(typeCategory);
List<String> typeNames = new ArrayList<>();
for (Entry<String, IDataType> typeEntry : types_.entrySet()) {
String name = typeEntry.getKey();
IDataType type = typeEntry.getValue();
if (type.getTypeCategory().equals(typeCategory)) {
typeNames.add(name);
}
}
return typeNames;
}
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#getAllNames()
*/
@Override
public Collection<String> getAllTypeNames() throws AtlasException {
return types_.keySet();
}
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#put(org.apache
* .atlas.typesystem.types.IDataType)
*/
@Override
public void put(IDataType type) throws AtlasException {
assertValidType(type);
types_.put(type.getName(), type);
}
private void assertValidType(IDataType type) throws
AtlasException {
if (type == null) {
throw new AtlasException("type is null.");
}
boolean validTypeCategory = (type instanceof ClassType) ||
(type instanceof TraitType) ||
(type instanceof EnumType) ||
(type instanceof StructType);
if (!validTypeCategory) {
throw new AtlasException("Category of the types should be one of ClassType | "
+ "TraitType | EnumType | StructType.");
}
}
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#putAll(java
* .util.Collection)
*/
@Override
public void putAll(Collection<IDataType> types) throws AtlasException {
for (IDataType type : types) {
assertValidType(type);
types_.put(type.getName(), type);
}
}
/*
* (non-Javadoc)
* @see
* org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#remove(java
* .lang.String)
*/
@Override
public void remove(String typeName) throws AtlasException {
types_.remove(typeName);
}
/* (non-Javadoc)
* @see org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#remove(org.
* apache.atlas.typesystem.types.DataTypes.TypeCategory, java.lang.String)
*/
@Override
public void remove(TypeCategory typeCategory, String typeName)
throws AtlasException {
assertValidTypeCategory(typeCategory);
remove(typeName);
}
/*
* (non-Javadoc)
* @see org.apache.atlas.typesystem.types.cache.ITypeCacheProvider#clear()
*/
@Override
public void clear() {
types_.clear();
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.atlas.typesystem.types.cache;
import java.util.Collection;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.types.DataTypes;
import org.apache.atlas.typesystem.types.IDataType;
/**
* The types are cached to allow faster lookup when type info is needed during
* creation/updation of entities, DSL query translation/execution.
* Implementations of this can chose to plugin a distributed cache provider
* or an in-memory cache synched across nodes in an Altas cluster. <br>
* <br>
* Type entries in the cache can be one of ... <br>
* {@link org.apache.atlas.typesystem.types.ClassType} <br>
* {@link org.apache.atlas.typesystem.types.TraitType} <br>
* {@link org.apache.atlas.typesystem.types.StructType} <br>
* {@link org.apache.atlas.typesystem.types.EnumType}
*/
@SuppressWarnings("rawtypes")
public interface ITypeCacheProvider {
/**
* @param typeName
* @return true if the type exists in cache, false otherwise.
* @throws AtlasException
*/
boolean has(String typeName) throws AtlasException;
/**
* @param typeCategory Non-null category of type. The category can be one of
* TypeCategory.CLASS | TypeCategory.TRAIT | TypeCategory.STRUCT | TypeCategory.ENUM.
* @param typeName
* @return true if the type of given category exists in cache, false otherwise.
* @throws AtlasException
*/
boolean has(DataTypes.TypeCategory typeCategory, String typeName) throws AtlasException;
/**
* @param name The name of the type.
* @return returns non-null type if cached, otherwise null
* @throws AtlasException
*/
public IDataType get(String typeName) throws AtlasException;
/**
* @param typeCategory Non-null category of type. The category can be one of
* TypeCategory.CLASS | TypeCategory.TRAIT | TypeCategory.STRUCT | TypeCategory.ENUM.
* @param typeName
* @return returns non-null type (of the specified category) if cached, otherwise null
* @throws AtlasException
*/
public IDataType get(DataTypes.TypeCategory typeCategory, String typeName) throws AtlasException;
/**
* @param typeCategory The category of types to filter the returned types. Cannot be null.
* The category can be one of TypeCategory.CLASS | TypeCategory.TRAIT |
* TypeCategory.STRUCT | TypeCategory.ENUM.
* @return
* @throws AtlasException
*/
Collection<String> getTypeNames(DataTypes.TypeCategory typeCategory) throws AtlasException;
/**
* This is a convenience API to get the names of all types.
*
* @see ITypeCacheProvider#getTypeNames(org.apache.atlas.typesystem.types.DataTypes.TypeCategory)
* @return
* @throws AtlasException
*/
Collection<String> getAllTypeNames() throws AtlasException;
/**
* @param type The type to be added to the cache. The type should not be
* null, otherwise throws NullPointerException. <br>
* Type entries in the cache can be one of ... <br>
* {@link org.apache.atlas.typesystem.types.ClassType} <br>
* {@link org.apache.atlas.typesystem.types.TraitType} <br>
* {@link org.apache.atlas.typesystem.types.StructType} <br>
* {@link org.apache.atlas.typesystem.types.EnumType}
* @throws AtlasException
*/
void put(IDataType type) throws AtlasException;
/**
* @param types The types to be added to the cache. The type should not be
* null, otherwise throws NullPointerException. <br>
* Type entries in the cache can be one of ... <br>
* {@link org.apache.atlas.typesystem.types.ClassType} <br>
* {@link org.apache.atlas.typesystem.types.TraitType} <br>
* {@link org.apache.atlas.typesystem.types.StructType} <br>
* {@link org.apache.atlas.typesystem.types.EnumType}
* @throws AtlasException
*/
void putAll(Collection<IDataType> types) throws AtlasException;
/**
* @param typeName Name of the type to be removed from the cache. If type
* exists, it will be removed, otherwise does nothing.
* @throws AtlasException
*/
void remove(String typeName) throws AtlasException;
/**
* @param typeCategory Non-null category of type. The category can be one of
* TypeCategory.CLASS | TypeCategory.TRAIT | TypeCategory.STRUCT | TypeCategory.ENUM.
* @param typeName Name of the type to be removed from the cache. If type
* exists, it will be removed, otherwise does nothing.
* @throws AtlasException
*/
void remove(DataTypes.TypeCategory typeCategory, String typeName) throws AtlasException;
/**
* Clear the type cache
*
*/
void clear();
}
......@@ -19,6 +19,7 @@ package org.apache.atlas;
import org.apache.atlas.typesystem.types.TypeSystem;
import org.apache.commons.configuration.Configuration;
import org.testng.Assert;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
......@@ -58,11 +59,20 @@ public class ApplicationPropertiesTest {
@Test
public void testGetClass() throws Exception {
//read from atlas-application.properties
Class cls = ApplicationProperties.getClass("atlas.TypeSystem.impl", ApplicationProperties.class.getName());
Class cls = ApplicationProperties.getClass("atlas.TypeSystem.impl", ApplicationProperties.class.getName(), TypeSystem.class);
assertEquals(cls.getName(), TypeSystem.class.getName());
//default value
cls = ApplicationProperties.getClass("atlas.TypeSystem2.impl", TypeSystem.class.getName());
cls = ApplicationProperties.getClass("atlas.TypeSystem2.impl", TypeSystem.class.getName(), TypeSystem.class);
assertEquals(cls.getName(), TypeSystem.class.getName());
//incompatible assignTo class, should throw AtlasException
try {
cls = ApplicationProperties.getClass("atlas.TypeSystem.impl", ApplicationProperties.class.getName(), ApplicationProperties.class);
Assert.fail(AtlasException.class.getSimpleName() + " was expected but none thrown.");
}
catch (AtlasException e) {
// good
}
}
}
......@@ -209,7 +209,7 @@ public class TypeSystemTest extends BaseTest {
}
@Test
public void testTypeNamesAreNotDuplicated() {
public void testTypeNamesAreNotDuplicated() throws Exception {
TypeSystem typeSystem = getTypeSystem();
ImmutableList<String> traitNames = typeSystem.getTypeNamesByCategory(DataTypes.TypeCategory.TRAIT);
int numTraits = traitNames.size();
......
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.atlas.typesystem.types.cache;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.atlas.AtlasException;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.DataTypes;
import org.apache.atlas.typesystem.types.DataTypes.TypeCategory;
import org.apache.atlas.typesystem.types.EnumType;
import org.apache.atlas.typesystem.types.EnumValue;
import org.apache.atlas.typesystem.types.IDataType;
import org.apache.atlas.typesystem.types.StructType;
import org.apache.atlas.typesystem.types.TraitType;
import org.apache.atlas.typesystem.types.TypeSystem;
import org.apache.atlas.typesystem.types.utils.TypesUtil;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableSet;
/**
* Tests functional behavior of {@link DefaultTypeCacheProvider}
*/
@SuppressWarnings("rawtypes")
public class DefaultTypeCacheProviderTest {
private String CLASSTYPE_CUSTOMER = "Customer";
private String STRUCTTYPE_ADDRESS = "Address";
private String TRAITTYPE_PRIVILEGED = "Privileged";
private String ENUMTYPE_SHIPPING = "Shipping";
private String UNKNOWN_TYPE = "UndefinedType";
private ClassType customerType;
private StructType addressType;
private TraitType privilegedTrait;
private EnumType shippingEnum;
private DefaultTypeCacheProvider cacheProvider;
@BeforeClass
public void onetimeSetup() throws Exception {
// init TypeSystem
TypeSystem ts = TypeSystem.getInstance().reset();
// Customer ClassType
customerType = ts.defineClassType(TypesUtil
.createClassTypeDef(CLASSTYPE_CUSTOMER, ImmutableSet.<String>of(),
TypesUtil.createRequiredAttrDef("name", DataTypes.STRING_TYPE),
TypesUtil.createRequiredAttrDef("id", DataTypes.LONG_TYPE)));
// Address StructType
addressType = ts.defineStructType(STRUCTTYPE_ADDRESS, true,
TypesUtil.createRequiredAttrDef("first line", DataTypes.STRING_TYPE),
TypesUtil.createOptionalAttrDef("second line", DataTypes.STRING_TYPE),
TypesUtil.createRequiredAttrDef("city", DataTypes.STRING_TYPE),
TypesUtil.createRequiredAttrDef("pincode", DataTypes.INT_TYPE));
// Privileged TraitType
privilegedTrait = ts.defineTraitType(TypesUtil
.createTraitTypeDef(TRAITTYPE_PRIVILEGED, ImmutableSet.<String>of(),
TypesUtil.createRequiredAttrDef("category", DataTypes.INT_TYPE)));
// Shipping EnumType
shippingEnum = ts.defineEnumType(TypesUtil.createEnumTypeDef(ENUMTYPE_SHIPPING,
new EnumValue("Domestic", 1), new EnumValue("International", 2)));
}
@BeforeMethod
public void eachTestSetup() throws Exception {
cacheProvider = new DefaultTypeCacheProvider();
cacheProvider.put(customerType);
cacheProvider.put(addressType);
cacheProvider.put(privilegedTrait);
cacheProvider.put(shippingEnum);
}
@Test
public void testCacheGetType() throws Exception {
IDataType custType = cacheProvider.get(CLASSTYPE_CUSTOMER);
verifyType(custType, CLASSTYPE_CUSTOMER, ClassType.class);
IDataType addrType = cacheProvider.get(STRUCTTYPE_ADDRESS);
verifyType(addrType, STRUCTTYPE_ADDRESS, StructType.class);
IDataType privTrait = cacheProvider.get(TRAITTYPE_PRIVILEGED);
verifyType(privTrait, TRAITTYPE_PRIVILEGED, TraitType.class);
IDataType shippingEnum = cacheProvider.get(ENUMTYPE_SHIPPING);
verifyType(shippingEnum, ENUMTYPE_SHIPPING, EnumType.class);
assertNull(cacheProvider.get(UNKNOWN_TYPE));
}
@Test
public void testCacheGetTypeByCategory() throws Exception {
IDataType custType = cacheProvider.get(TypeCategory.CLASS, CLASSTYPE_CUSTOMER);
verifyType(custType, CLASSTYPE_CUSTOMER, ClassType.class);
IDataType addrType = cacheProvider.get(TypeCategory.STRUCT, STRUCTTYPE_ADDRESS);
verifyType(addrType, STRUCTTYPE_ADDRESS, StructType.class);
IDataType privTrait = cacheProvider.get(TypeCategory.TRAIT, TRAITTYPE_PRIVILEGED);
verifyType(privTrait, TRAITTYPE_PRIVILEGED, TraitType.class);
IDataType shippingEnum = cacheProvider.get(TypeCategory.ENUM, ENUMTYPE_SHIPPING);
verifyType(shippingEnum, ENUMTYPE_SHIPPING, EnumType.class);
assertNull(cacheProvider.get(UNKNOWN_TYPE));
}
private void verifyType(IDataType actualType, String expectedName, Class<? extends IDataType> typeClass) {
assertNotNull(actualType, "The " + expectedName + " type not in cache");
assertTrue(typeClass.isInstance(actualType));
assertEquals(actualType.getName(), expectedName, "The type name does not match");
}
@Test
public void testCacheHasType() throws Exception {
assertTrue(cacheProvider.has(CLASSTYPE_CUSTOMER));
assertTrue(cacheProvider.has(STRUCTTYPE_ADDRESS));
assertTrue(cacheProvider.has(TRAITTYPE_PRIVILEGED));
assertTrue(cacheProvider.has(ENUMTYPE_SHIPPING));
assertFalse(cacheProvider.has(UNKNOWN_TYPE));
}
@Test
public void testCacheHasTypeByCategory() throws Exception {
assertTrue(cacheProvider.has(TypeCategory.CLASS, CLASSTYPE_CUSTOMER));
assertTrue(cacheProvider.has(TypeCategory.STRUCT, STRUCTTYPE_ADDRESS));
assertTrue(cacheProvider.has(TypeCategory.TRAIT, TRAITTYPE_PRIVILEGED));
assertTrue(cacheProvider.has(TypeCategory.ENUM, ENUMTYPE_SHIPPING));
assertFalse(cacheProvider.has(UNKNOWN_TYPE));
}
@Test
public void testCacheGetAllTypeNames() throws Exception {
List<String> allTypeNames = new ArrayList<String>(cacheProvider.getAllTypeNames());
Collections.sort(allTypeNames);
final int EXPECTED_TYPE_COUNT = 4;
assertEquals(allTypeNames.size(), EXPECTED_TYPE_COUNT, "Total number of types does not match.");
assertEquals(STRUCTTYPE_ADDRESS, allTypeNames.get(0));
assertEquals(CLASSTYPE_CUSTOMER, allTypeNames.get(1));
assertEquals(TRAITTYPE_PRIVILEGED, allTypeNames.get(2));
assertEquals(ENUMTYPE_SHIPPING, allTypeNames.get(3));
}
@Test
public void testCacheGetTypeNamesByCategory() throws Exception {
List<String> classTypes = new ArrayList<String>(cacheProvider.getTypeNames(TypeCategory.CLASS));
final int EXPECTED_CLASSTYPE_COUNT = 1;
assertEquals(classTypes.size(), EXPECTED_CLASSTYPE_COUNT);
assertEquals(CLASSTYPE_CUSTOMER, classTypes.get(0));
List<String> structTypes = new ArrayList<String>(cacheProvider.getTypeNames(TypeCategory.STRUCT));
final int EXPECTED_STRUCTTYPE_COUNT = 1;
assertEquals(structTypes.size(), EXPECTED_STRUCTTYPE_COUNT);
assertEquals(STRUCTTYPE_ADDRESS, structTypes.get(0));
List<String> traitTypes = new ArrayList<String>(cacheProvider.getTypeNames(TypeCategory.TRAIT));
final int EXPECTED_TRAITTYPE_COUNT = 1;
assertEquals(traitTypes.size(), EXPECTED_TRAITTYPE_COUNT);
assertEquals(TRAITTYPE_PRIVILEGED, traitTypes.get(0));
List<String> enumTypes = new ArrayList<String>(cacheProvider.getTypeNames(TypeCategory.ENUM));
final int EXPECTED_ENUMTYPE_COUNT = 1;
assertEquals(enumTypes.size(), EXPECTED_ENUMTYPE_COUNT);
assertEquals(ENUMTYPE_SHIPPING, enumTypes.get(0));
}
@Test
public void testCacheBulkInsert() throws Exception {
List<IDataType> allTypes = new ArrayList<>();
allTypes.add(customerType);
allTypes.add(addressType);
allTypes.add(privilegedTrait);
allTypes.add(shippingEnum);
// create a new cache provider instead of using the one setup for every method call
cacheProvider = new DefaultTypeCacheProvider();
cacheProvider.putAll(allTypes);
IDataType custType = cacheProvider.get(CLASSTYPE_CUSTOMER);
verifyType(custType, CLASSTYPE_CUSTOMER, ClassType.class);
IDataType addrType = cacheProvider.get(STRUCTTYPE_ADDRESS);
verifyType(addrType, STRUCTTYPE_ADDRESS, StructType.class);
IDataType privTrait = cacheProvider.get(TRAITTYPE_PRIVILEGED);
verifyType(privTrait, TRAITTYPE_PRIVILEGED, TraitType.class);
IDataType shippingEnum = cacheProvider.get(ENUMTYPE_SHIPPING);
verifyType(shippingEnum, ENUMTYPE_SHIPPING, EnumType.class);
}
@Test
public void testCacheRemove() throws Exception {
cacheProvider.remove(CLASSTYPE_CUSTOMER);
assertNull(cacheProvider.get(CLASSTYPE_CUSTOMER));
assertFalse(cacheProvider.has(CLASSTYPE_CUSTOMER));
assertTrue(cacheProvider.getTypeNames(TypeCategory.CLASS).isEmpty());
final int EXPECTED_TYPE_COUNT = 3;
assertEquals(cacheProvider.getAllTypeNames().size(), EXPECTED_TYPE_COUNT);
}
@Test
public void testCacheRemoveByCategory() throws Exception {
cacheProvider.remove(TypeCategory.CLASS, CLASSTYPE_CUSTOMER);
assertNull(cacheProvider.get(CLASSTYPE_CUSTOMER));
assertFalse(cacheProvider.has(CLASSTYPE_CUSTOMER));
assertTrue(cacheProvider.getTypeNames(TypeCategory.CLASS).isEmpty());
final int EXPECTED_TYPE_COUNT = 3;
assertEquals(cacheProvider.getAllTypeNames().size(), EXPECTED_TYPE_COUNT);
}
@Test
public void testCacheClear() throws Exception {
cacheProvider.clear();
assertNull(cacheProvider.get(CLASSTYPE_CUSTOMER));
assertFalse(cacheProvider.has(CLASSTYPE_CUSTOMER));
assertNull(cacheProvider.get(STRUCTTYPE_ADDRESS));
assertFalse(cacheProvider.has(STRUCTTYPE_ADDRESS));
assertNull(cacheProvider.get(TRAITTYPE_PRIVILEGED));
assertFalse(cacheProvider.has(TRAITTYPE_PRIVILEGED));
assertNull(cacheProvider.get(ENUMTYPE_SHIPPING));
assertFalse(cacheProvider.has(ENUMTYPE_SHIPPING));
assertTrue(cacheProvider.getTypeNames(TypeCategory.CLASS).isEmpty());
assertTrue(cacheProvider.getTypeNames(TypeCategory.STRUCT).isEmpty());
assertTrue(cacheProvider.getTypeNames(TypeCategory.TRAIT).isEmpty());
assertTrue(cacheProvider.getTypeNames(TypeCategory.ENUM).isEmpty());
assertTrue(cacheProvider.getAllTypeNames().isEmpty());
}
@Test(expectedExceptions = AtlasException.class)
public void testPutTypeWithNullType() throws Exception {
cacheProvider.put(null);
fail("Null type should be not allowed in 'put'");
}
@Test(expectedExceptions = AtlasException.class)
public void testPutTypeWithInvalidType() throws Exception {
cacheProvider.put(DataTypes.BOOLEAN_TYPE);
fail("type should only be an instance of ClassType | EnumType | StructType | TraitType in 'put'");
}
@Test(expectedExceptions = AtlasException.class)
public void testGetTypeWithNullCategory() throws Exception {
cacheProvider.get(null, CLASSTYPE_CUSTOMER);
fail("Null TypeCategory should be not allowed in 'get'");
}
@Test(expectedExceptions = AtlasException.class)
public void testGetTypeWithInvalidCategory() throws Exception {
cacheProvider.get(TypeCategory.PRIMITIVE, DataTypes.BOOLEAN_TYPE.getName());
fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'get'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheHasTypeWithNullCategory() throws Exception {
cacheProvider.has(null, CLASSTYPE_CUSTOMER);
fail("Null TypeCategory should be not allowed in 'has'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheHasTypeWithInvalidCategory() throws Exception {
cacheProvider.has(TypeCategory.PRIMITIVE, DataTypes.BOOLEAN_TYPE.getName());
fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'has'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheGetTypeNamesByNullCategory() throws Exception {
cacheProvider.getTypeNames(null);
fail("Null TypeCategory should be not allowed in 'getNames'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheGetTypeNamesByInvalidCategory() throws Exception {
cacheProvider.getTypeNames(TypeCategory.PRIMITIVE);
fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'getNames'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheBulkInsertWithNullType() throws Exception {
List<IDataType> allTypes = new ArrayList<>();
allTypes.add(null);
// create a new cache provider instead of using the one setup for every method call
cacheProvider = new DefaultTypeCacheProvider();
cacheProvider.putAll(allTypes);
fail("Null type should be not allowed in 'putAll'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheBulkInsertWithInvalidType() throws Exception {
List<IDataType> allTypes = new ArrayList<>();
allTypes.add(DataTypes.BOOLEAN_TYPE);
// create a new cache provider instead of using the one setup for every method call
cacheProvider = new DefaultTypeCacheProvider();
cacheProvider.putAll(allTypes);
fail("type should only one of ClassType | EnumType | StructType | TraitType in 'putAll'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheRemoveByNullCategory() throws Exception {
cacheProvider.remove(null, CLASSTYPE_CUSTOMER);
fail("Null type should be not allowed in 'remove'");
}
@Test(expectedExceptions = AtlasException.class)
public void testCacheRemoveByInvalidCategory() throws Exception {
cacheProvider.remove(TypeCategory.PRIMITIVE, DataTypes.BOOLEAN_TYPE.getName());
fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'remove'");
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment