Commit c3d16277 authored by T.J. Corona's avatar T.J. Corona

Introduce Properties for Resources and Components

Resources and components now have access to Properties, a
dictionary-like container that can hold any copy-constructible
type. Currently enabled types include long, double, std::string, and
std::vectors of these three types. Resource and component properties
replace and extend the functionality of smtk::model's property system,
while maintaining much of smtk::model's property API.
parent 39a1f8da
## Introduction of Properties for Resources and Components
Resources and components now have access to Properties, a
dictionary-like container that can hold any copy-constructible
type. Currently enabled types include long, double, std::string, and
std::vectors of these three types. Resource and component properties
replace and extend the functionality of smtk::model's property system,
while maintaining much of smtk::model's property API. For more
information, see the Properties section of the Resources description
in the user guide.
......@@ -4,10 +4,10 @@ Model Property System
=====================
In addition to associating modeling entities with attributes,
SMTK's model manager can also store string, integer, and floating-point
properties on model entities.
SMTK's model resource uses the resource's properties mechanism to
store string, integer, and floating-point properties on model entities.
Unlike attributes that have a rigid format imposed by definitions,
model properties are free-form: given any model entity UUID and a
properties are free-form: given any model entity UUID and a
property name, you may store any combination of string, integer, and
floating-point values.
......
......@@ -36,3 +36,19 @@ components, the subclass asks its parent resource for the links object
in order to search for connections. Roles are simply integers. Roles
less than zero are reserved for internal SMTK use while positive roles
are available for user-specified (i.e., application-specific) roles.
:smtk:`Properties <smtk::resource::Properties>`
Resources and components contain a dictionary-like data structure that
can hold any copyable type. If the property type has JSON bindings, it
will be serialized to file along with the resource; otherwise, the
property can still be stored, but will not be persistent. Currently
enabled types include long, double, std::string, and std::vectors of
these three types.
Properties are stored separately from the objects they annotate, but
each resource instance owns the properties instance that holds all
properties for the resource and its components. The
smtk::resource::Properties class is an abstract API provided on both
resources and components; on the resource, the properties subclass
provides actual storage, while on components, the subclass asks its
parent resource for the properties object to search for values.
......@@ -143,6 +143,58 @@ struct remove_from_tuple<T, std::tuple<> >
{
using type = std::tuple<>;
};
/// Takes a type and a tuple of types and returns the index of the first
/// instance of that type in the tuple.
///
/// Examples:
/// tuple_index<bool, std::tuple<int, bool, float>>::value == 1
template <class T, class Tuple>
struct tuple_index;
template <class T, class... Types>
struct tuple_index<T, std::tuple<T, Types...> >
{
constexpr static const std::size_t value = 0;
};
template <class T, class U, class... Types>
struct tuple_index<T, std::tuple<U, Types...> >
{
constexpr static const std::size_t value = 1 + tuple_index<T, std::tuple<Types...> >::value;
};
/// Takes a type and a tuple of types and returns a bool indicating whether or
/// not the type is in the tuple.
///
/// Examples:
/// tuple_has<bool, std::tuple<int, bool, float>>() == true
/// tuple_has<bool, std::tuple<int, float>>() == false
template <typename T, typename Tuple>
struct tuple_has;
template <typename T>
struct tuple_has<T, std::tuple<> > : std::false_type
{
};
template <typename T, typename U, typename... Ts>
struct tuple_has<T, std::tuple<U, Ts...> > : tuple_has<T, std::tuple<Ts...> >
{
};
template <typename T, typename... Ts>
struct tuple_has<T, std::tuple<T, Ts...> > : std::true_type
{
};
/// Embeds a type in another class so its type information can be passed as a
/// parameter.
template <typename T>
struct identity
{
typedef T type;
};
}
#endif
......@@ -8,6 +8,7 @@ set(commonSrcs
Extension.cxx
FileLocation.cxx
json/jsonLinks.cxx
json/jsonProperties.cxx
json/jsonUUID.cxx
Paths.cxx
Registry.cxx
......@@ -29,10 +30,12 @@ set(commonHeaders
Generator.h
GeometryUtilities.h
json/jsonLinks.h
json/jsonProperties.h
json/jsonUUID.h
Links.h
Observers.h
Paths.h
Properties.h
RangeDetector.h
Registry.h
Singleton.h
......
//=========================================================================
// Copyright (c) Kitware, Inc.
// All rights reserved.
// See LICENSE.txt for details.
//
// This software is distributed WITHOUT ANY WARRANTY; without even
// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the above copyright notice for more information.
//=========================================================================
#ifndef smtk_common_Properties_h
#define smtk_common_Properties_h
#include "smtk/CoreExports.h"
#include "smtk/common/CompilerInformation.h"
#include "smtk/common/TypeName.h"
SMTK_THIRDPARTY_PRE_INCLUDE
#include "nlohmann/json.hpp"
SMTK_THIRDPARTY_POST_INCLUDE
#include <memory>
#include <string>
#include <type_traits>
#include <unordered_map>
namespace smtk
{
namespace common
{
/// A common base class for properties of a given type. The PropertiesContainer
/// class is a collection of instances of PropertiesOfType, necessitating a
/// common base for their aggregate storage. An API for serialization is
/// included to avoid having to upcast to a templated type.
class SMTKCORE_EXPORT PropertiesBase
{
public:
virtual ~PropertiesBase() {}
virtual void to_json(nlohmann::json&) const {}
virtual void from_json(const nlohmann::json&) {}
};
class Properties;
/// A specialization of the PropertiesContainer for a single type.
/// PropertiesOfType provides a non-templated API for accessing property
/// information, as well as serialization logic if the underlying type is
/// serializable.
template <typename Type>
class PropertiesOfType : public PropertiesBase
{
friend class Properties;
protected:
/// Construction of this class is delegated to Properties.
PropertiesOfType() {}
public:
typedef std::string key_type;
typedef Type mapped_type;
/// As an extension of PropertiesContainer's API, copy & move construction are
/// not supported.
PropertiesOfType(const PropertiesOfType&) = delete;
PropertiesOfType(PropertiesOfType&&) = delete;
PropertiesOfType& operator=(const PropertiesOfType&) = delete;
PropertiesOfType& operator=(PropertiesOfType&&) = delete;
/// Check whether a property associated with <key> is present.
bool has(const std::string& key) const { return (m_data.find(key) != m_data.end()); }
/// Insert (<key>, <value>) into the container.
bool insert(const std::string& key, const Type& value)
{
return m_data.insert(std::make_pair(key, value)).second;
}
/// Emplace (<key>, <value>) into the container.
bool emplace(const std::string& key, Type&& value)
{
return m_data.emplace(std::make_pair(key, std::move(value))).second;
}
/// Erase property indexed by <key> from the container.
void erase(const std::string& key) { m_data.erase(key); }
/// Access property indexed by <key>.
Type& operator[](const std::string& key) { return m_data[key]; }
/// Access property indexed by <key>.
Type& at(const std::string& key) { return m_data.at(key); }
/// Access property indexed by <key>.
const Type& at(const std::string& key) const { return m_data.at(key); }
/// Access the class's underlying data.
std::unordered_map<std::string, Type>& data() { return m_data; }
const std::unordered_map<std::string, Type>& data() const { return m_data; }
void to_json(nlohmann::json& j) const override { return to_json<Type>(j); }
void from_json(const nlohmann::json& j) override { return from_json<Type>(j); }
private:
template <typename T>
typename std::enable_if<nlohmann::detail::is_compatible_type<nlohmann::json, T>::value>::type
to_json(nlohmann::json& j) const
{
j = m_data;
}
template <typename T>
typename std::enable_if<!nlohmann::detail::is_compatible_type<nlohmann::json, T>::value>::type
to_json(nlohmann::json&) const
{
}
template <typename T>
typename std::enable_if<nlohmann::detail::is_compatible_type<nlohmann::json, T>::value>::type
from_json(const nlohmann::json& j)
{
m_data = j.get<decltype(m_data)>();
}
template <typename T>
typename std::enable_if<!nlohmann::detail::is_compatible_type<nlohmann::json, T>::value>::type
from_json(const nlohmann::json&) const
{
}
std::unordered_map<std::string, Type> m_data;
};
/// The PropertiesContainer class holds the storage and API for Properties. It
/// is decoupled from Properties to facilitate reuse by downstream classes that
/// can derive from PropertiesOfType<>. PropertiesContainer does not contain any
/// methods to insert types (this functionality is delegated to the downstream
/// Container class).
class SMTKCORE_EXPORT PropertiesContainer
{
public:
virtual ~PropertiesContainer() = 0;
/// Check whether a property of type <Type> associated with <key> is present.
/// On average, this method has constant complexity and can therefore be used
/// in conjunction with at() for conditional queries.
template <typename Type>
bool has(const std::string& key) const
{
auto it = m_data.find(smtk::common::typeName<Type>());
if (it == m_data.end())
{
return false;
}
return static_cast<const PropertiesOfType<Type>&>(*it->second).has(key);
}
/// Insert (<Type>, <key>, <value>) into the container.
template <typename Type>
bool insert(const std::string& key, const Type& value)
{
return get<Type>().insert(key, value);
}
/// Emplace (<Type>, <key>, <value>) into the container.
template <typename Type>
bool emplace(const std::string& key, Type&& value)
{
return get<Type>().emplace(key, std::move(value));
}
/// Erase property of type <Type> indexed by <key> from the container.
template <typename Type>
void erase(const std::string& key)
{
auto& property = get<Type>();
property.erase(key);
}
/// Access property of type <Type> indexed by <key>.
/// On average, this method has constant complexity and can therefore be used
/// in conjunction with has() for conditional queries.
template <typename Type>
Type& at(const std::string& key)
{
return get<Type>().at(key);
}
/// Access property of type <Type> indexed by <key>.
/// On average, this method has constant complexity and can therefore be used
/// in conjunction with has() for conditional queries.
template <typename Type>
const Type& at(const std::string& key) const
{
return get<Type>().at(key);
}
/// Access properties of type <Type>.
template <typename Type>
PropertiesOfType<Type>& get()
{
std::string key = smtk::common::typeName<Type>();
auto it = m_data.find(key);
if (it == m_data.end())
{
throw std::domain_error("No property with given type");
}
return static_cast<PropertiesOfType<Type>&>(*it->second);
}
/// Access properties of type <Type>.
template <typename Type>
const PropertiesOfType<Type>& get() const
{
auto it = m_data.find(smtk::common::typeName<Type>());
if (it == m_data.end())
{
throw std::domain_error("No property with given type");
}
return static_cast<const PropertiesOfType<Type>&>(*it->second);
}
/// Check whether property type <Type> is supported.
template <typename Type>
bool hasPropertyType() const
{
return (m_data.find(smtk::common::typeName<Type>()) != m_data.end());
}
/// Access the class's underlying data.
std::unordered_map<std::string, PropertiesBase*>& data() { return m_data; }
const std::unordered_map<std::string, PropertiesBase*>& data() const { return m_data; }
private:
std::unordered_map<std::string, PropertiesBase*> m_data;
};
inline PropertiesContainer::~PropertiesContainer()
{
for (auto& pair : m_data)
{
delete pair.second;
}
}
/// Properties is a generalized container for storing and accessing data using a
/// std::string key. The value type is open-ended and extensible; to accommodate
/// this flexibility, there is both a templated API on the PropertiesContainer
/// class and a specialized PropertiesOfType interface. Properties augments
/// PropertiesContainer with the ability to declare supported types; this
/// functionality is only exposed at construction to enforce RAII (otherwise,
/// serialization routines would have to bind type names to types).
class SMTKCORE_EXPORT Properties : public PropertiesContainer
{
public:
Properties() {}
template <typename List>
Properties()
{
insertPropertyTypes<List>();
}
template <typename List>
Properties(identity<List>)
{
insertPropertyTypes<List>();
}
~Properties() {}
protected:
template <typename Type>
void insertPropertyType()
{
std::string key = smtk::common::typeName<Type>();
auto it = data().find(key);
if (it == data().end())
{
it = data().emplace(std::make_pair(key, new PropertiesOfType<Type>)).first;
}
}
template <typename Tuple>
void insertPropertyTypes()
{
Properties::insertPropertyTypes<0, Tuple>();
}
private:
template <std::size_t I, typename Tuple>
inline typename std::enable_if<I != std::tuple_size<Tuple>::value>::type insertPropertyTypes()
{
this->insertPropertyType<typename std::tuple_element<I, Tuple>::type>();
Properties::insertPropertyTypes<I + 1, Tuple>();
}
template <std::size_t I, typename Tuple>
inline typename std::enable_if<I == std::tuple_size<Tuple>::value>::type insertPropertyTypes()
{
}
};
}
}
#endif
......@@ -11,13 +11,57 @@
#ifndef smtk_common_TypeName_h
#define smtk_common_TypeName_h
#include "smtk/TupleTraits.h"
#include <array>
#include <map>
#include <set>
#include <string>
#include <tuple>
#include <typeinfo>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace smtk
{
namespace common
{
namespace detail
{
static constexpr const char* const char_name = "char";
static constexpr const char* const unsigned_char_name = "unsigned char";
static constexpr const char* const signed_char_name = "signed char";
static constexpr const char* const int_name = "int";
static constexpr const char* const unsigned_int_name = "unsigned int";
static constexpr const char* const signed_int_name = "signed int";
static constexpr const char* const short_int_name = "short int";
static constexpr const char* const unsigned_short_int_name = "unsigned short int";
static constexpr const char* const signed_short_int_name = "signed short int";
static constexpr const char* const long_int_name = "long int";
static constexpr const char* const signed_long_int_name = "signed long int";
static constexpr const char* const unsigned_long_int_name = "unsigned long int";
static constexpr const char* const float_name = "float";
static constexpr const char* const double_name = "double";
static constexpr const char* const long_double_name = "long double";
static constexpr const char* const wchar_t_name = "wchar_t";
static constexpr const char* const std_string_name = "string";
}
/// For serialization across platforms, we define platform-neutral type names
/// for plain-old-datatypes (PODs) and for std::string.
typedef std::tuple<char, unsigned char, signed char, int, unsigned int, signed int, short int,
unsigned short int, signed short int, long int, signed long int, unsigned long int, float, double,
long double, wchar_t, std::string>
PODs;
typedef std::array<const char* const, std::tuple_size<PODs>::value> PODNames_t;
static constexpr PODNames_t PODNames = { { detail::char_name, detail::unsigned_char_name,
detail::signed_char_name, detail::int_name, detail::unsigned_int_name, detail::signed_int_name,
detail::short_int_name, detail::unsigned_short_int_name, detail::signed_short_int_name,
detail::long_int_name, detail::signed_long_int_name, detail::unsigned_long_int_name,
detail::float_name, detail::double_name, detail::long_double_name, detail::wchar_t_name,
detail::std_string_name } };
/// Resources and operations have a virtual method typeName(), but to access
/// this value we must create an instance of the class. Alternatively, these
......@@ -27,7 +71,7 @@ namespace common
/// tandem with typeName(). To relax the requirements of a) a macro definition
/// in a class header, or b) a mysterious constexpr in a class declaration,
/// the free function name() will traverse one of two code paths to determine a
/// type name.
/// type name for a user-defined type.
namespace detail
{
......@@ -44,6 +88,14 @@ public:
using type = decltype(testNamed<T>(nullptr));
};
// A compile-time test to check whether or not a type is in our list of PODs.
template <typename T>
struct is_pod
{
using type = typename smtk::tuple_has<T, PODs>::type;
};
// A compile-time test to check whether or not a class has a create() method.
template <typename T>
class is_constructible
{
......@@ -56,37 +108,113 @@ public:
using type = decltype(testConstructible<T>(nullptr));
};
// The signature for our name-finding struct has two template parameters.
template <typename Type, typename is_named>
// The signature for our name-finding struct has four template parameters.
template <typename Type, typename is_named, typename is_pod, typename is_constructible>
struct name;
// This partial template specialization deals with the case where
// <Type> does not have a type_name. In this case, we create a temporary
// object and ask for its typeName.
// <Type> has a type_name.
template <typename Type, typename is_pod, typename is_constructible>
struct name<Type, std::true_type, is_pod, is_constructible>
{
static std::string value() { return Type::type_name; }
};
// This partial template specialization deals with the case where
// <Type> is a POD.
template <typename Type, typename is_constructible>
struct name<Type, std::false_type, std::true_type, is_constructible>
{
static std::string value() { return std::string(PODNames.at(tuple_index<Type, PODs>::value)); }
};
// This partial template specialization deals with the case where
// <Type> does not have a type_name and is not a POD, but can be created.
template <typename Type>
struct name<Type, std::false_type>
struct name<Type, std::false_type, std::false_type, std::true_type>
{
static std::string value() { return Type::create()->typeName(); }
};
static typename std::enable_if<is_constructible<Type>::type, std::string>::type value()
// As a last resort, we return the machine-dependent name for the type.
template <typename Type>
struct name<Type, std::false_type, std::false_type, std::false_type>
{
static std::string value() { return typeid(Type).name(); }
};
// This partial template specialization provides support for vectors of named
// types.
template <typename Type>
struct name<std::vector<Type>, std::false_type, std::false_type, std::false_type>
{
static std::string value()
{
return Type::create()->typeName();
std::string subtype = name<Type, typename detail::is_named<Type>::type,
typename detail::is_pod<Type>::type, typename detail::is_constructible<Type>::type>::value();
return std::string("vector<" + subtype + ">");
}
};
// As a last resort, we return the machine-dependent name for tye type.
static typename std::enable_if<!is_constructible<Type>::type, std::string>::type value(int i = 0)
// This partial template specialization provides support for sets of named
// types.
template <typename Type>
struct name<std::set<Type>, std::false_type, std::false_type, std::false_type>
{
static std::string value()
{
(void)i;
return typeid(Type).name();
std::string subtype = name<Type, typename detail::is_named<Type>::type,
typename detail::is_pod<Type>::type, typename detail::is_constructible<Type>::type>::value();
return std::string("set<" + subtype + ">");
}
};
// This partial template specialization deals with the case where
// <Type> has a type_name. In this case, we can return the class type
// name without instantiating the class.
// This partial template specialization provides support for unordered sets of
// named types.
template <typename Type>
struct name<Type, std::true_type>
struct name<std::unordered_set<Type>, std::false_type, std::false_type, std::false_type>
{
static std::string value() { return Type::type_name; }
static std::string value()
{
std::string subtype = name<Type, typename detail::is_named<Type>::type,
typename detail::is_pod<Type>::type, typename detail::is_constructible<Type>::type>::value();
return std::string("unordered set<" + subtype + ">");
}
};
// This partial template specialization provides support for maps of named
// types.
template <typename KeyType, typename ValueType>
struct name<std::map<KeyType, ValueType>, std::false_type, std::false_type, std::false_type>
{
static std::string value()
{
std::string keytype = name<KeyType, typename detail::is_named<KeyType>::type,
typename detail::is_pod<KeyType>::type,
typename detail::is_constructible<KeyType>::type>::value();
std::string valuetype = name<ValueType, typename detail::is_named<ValueType>::type,
typename detail::is_pod<ValueType>::type,
typename detail::is_constructible<ValueType>::type>::value();
return std::string("map<" + keytype + ", " + valuetype + ">");
}
};
// This partial template specialization provides support for unordered maps of
// named types.
template <typename KeyType, typename ValueType>
struct name<std::unordered_map<KeyType, ValueType>, std::false_type, std::false_type,
std::false_type>
{
static std::string value()
{
std::string keytype = name<KeyType, typename detail::is_named<KeyType>::type,
typename detail::is_pod<KeyType>::type,
typename detail::is_constructible<KeyType>::type>::value();
std::string valuetype = name<ValueType, typename detail::is_named<ValueType>::type,
typename detail::is_pod<ValueType>::type,
typename detail::is_constructible<ValueType>::type>::value();
return std::string("unordered map<" + keytype + ", " + valuetype + ">");
}
};
}
......@@ -94,7 +222,8 @@ struct name<Type, std::true_type>
template <typename Type>
std::string typeName()
{
return detail::name<Type, typename detail::is_named<Type>::type>::value();
return detail::name<Type, typename detail::is_named<Type>::type,
typename detail::is_pod<Type>::type, typename detail::is_constructible<Type>::type>::value();
}
}
}
......
......@@ -38,6 +38,7 @@ namespace common
class SMTKCORE_EXPORT UUID
{
public:
static constexpr const char* const type_name = "uuid";
typedef ::boost::uint8_t value_type;
typedef ::boost::uint8_t* iterator;
typedef ::boost::uint8_t const* const_iterator;
......
//=========================================================================
// Copyright (c) Kitware, Inc.
// All rights reserved.
// See LICENSE.txt for details.
//
// This software is distributed WITHOUT ANY WARRANTY; without even
// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the above copyright notice for more information.
//=========================================================================
#include "smtk/common/json/jsonProperties.h"
#include <string>
namespace smtk
{
namespace common
{
void to_json(nlohmann::json& j, const PropertiesContainer& properties)
{
for (auto& property : properties.data())
{
std::string propertyName = property.first;
const PropertiesBase& base = *property.second;
auto& base_j = j[propertyName];
base.to_json(base_j);
if (base_j == nullptr)
{
j.erase(propertyName);
}
}
}
void from_json(const nlohmann::json& j, PropertiesContainer& properties)
{
for (auto& property : properties.data())
{
auto it = j.find(property.first);
if (it != j.end())
{
property.second->from_json(*it);
}