diff --git a/examples/simple_example/CustomCtrl.cc b/examples/simple_example/CustomCtrl.cc new file mode 100644 index 00000000..63f2f11c --- /dev/null +++ b/examples/simple_example/CustomCtrl.cc @@ -0,0 +1,11 @@ +#include "CustomCtrl.h" +//add definition of your processing function here + +void CustomCtrl::hello(const HttpRequestPtr &req, + const std::function &callback, + const std::string &userName) const +{ + auto resp = HttpResponse::newHttpResponse(); + resp->setBody("

" + _greetings + ", " + userName + "

"); + callback(resp); +} diff --git a/examples/simple_example/CustomCtrl.h b/examples/simple_example/CustomCtrl.h new file mode 100644 index 00000000..12e7e99d --- /dev/null +++ b/examples/simple_example/CustomCtrl.h @@ -0,0 +1,18 @@ +#pragma once +#include +using namespace drogon; +class CustomCtrl : public drogon::HttpController +{ + public: + METHOD_LIST_BEGIN + //use METHOD_ADD to add your custom processing function here; + METHOD_ADD(CustomCtrl::hello, "/{1}", Get, "CustomHeaderFilter"); //path is /customctrl/{arg1} + METHOD_LIST_END + + explicit CustomCtrl(const std::string &greetings) : _greetings(greetings) {} + + void hello(const HttpRequestPtr &req, const std::function &callback, const std::string &userName) const; + + private: + std::string _greetings; +}; diff --git a/examples/simple_example/CustomHeaderFilter.cc b/examples/simple_example/CustomHeaderFilter.cc new file mode 100644 index 00000000..75f4d3ba --- /dev/null +++ b/examples/simple_example/CustomHeaderFilter.cc @@ -0,0 +1,25 @@ +/** + * + * CustomHeaderFilter.cc + * + */ + +#include "CustomHeaderFilter.h" + +using namespace drogon; + +void CustomHeaderFilter::doFilter(const HttpRequestPtr &req, + const FilterCallback &fcb, + const FilterChainCallback &fccb) +{ + if (req->getHeader(_field) == _value) + { + //Passed + fccb(); + return; + } + //Check failed + auto res = drogon::HttpResponse::newHttpResponse(); + res->setStatusCode(k500InternalServerError); + fcb(res); +} diff --git a/examples/simple_example/CustomHeaderFilter.h b/examples/simple_example/CustomHeaderFilter.h new file mode 100644 index 00000000..83b2c5d0 --- /dev/null +++ b/examples/simple_example/CustomHeaderFilter.h @@ -0,0 +1,25 @@ +/** + * + * CustomHeaderFilter.h + * + */ + +#pragma once + +#include +using namespace drogon; + +class CustomHeaderFilter : public HttpFilter +{ + public: + CustomHeaderFilter(const std::string &field, const std::string &value) + : _field(field), + _value(value) {} + virtual void doFilter(const HttpRequestPtr &req, + const FilterCallback &fcb, + const FilterChainCallback &fccb) override; + + private: + std::string _field; + std::string _value; +}; diff --git a/examples/simple_example/main.cc b/examples/simple_example/main.cc index 13ae8be0..267ea630 100755 --- a/examples/simple_example/main.cc +++ b/examples/simple_example/main.cc @@ -2,6 +2,8 @@ #include #include #include +#include "CustomCtrl.h" +#include "CustomHeaderFilter.h" using namespace drogon; class A : public DrObjectBase @@ -141,9 +143,15 @@ int main() //drogon::HttpAppFramework::instance().enableDynamicViewsLoading({"/tmp/views"}); app().loadConfigFile("config.example.json"); auto &json = app().getCustomConfig(); - if(json.empty()) + if (json.empty()) { std::cout << "empty custom config!" << std::endl; } + //Install custom controller + auto ctrlPtr = std::make_shared("Hi"); + app().registerController(ctrlPtr); + //Install custom filter + auto filterPtr = std::make_shared("custom_header", "yes"); + app().registerFilter(filterPtr); app().run(); } diff --git a/examples/simple_example_test/main.cc b/examples/simple_example_test/main.cc index 45e8b488..79f644f7 100644 --- a/examples/simple_example_test/main.cc +++ b/examples/simple_example_test/main.cc @@ -532,7 +532,32 @@ void doTest(const HttpClientPtr &client, std::promise &pro, bool isHttps = exit(1); } }); - + /// Test controllers created and initialized by users + req = HttpRequest::newHttpFormPostRequest(); + req->setMethod(drogon::Get); + req->setPath("/customctrl/antao"); + req->addHeader("custom_header", "yes"); + client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) { + if (result == ReqResult::Ok) + { + auto ret = resp->getBody(); + if (ret == "

Hi, antao

") + { + outputGood(req, isHttps); + } + else + { + LOG_DEBUG << resp->getBody(); + LOG_ERROR << "Error!"; + exit(1); + } + } + else + { + LOG_ERROR << "Error!"; + exit(1); + } + }); /// Test form post req = HttpRequest::newHttpFormPostRequest(); req->setPath("/api/v1/apitest/form"); diff --git a/lib/inc/drogon/DrClassMap.h b/lib/inc/drogon/DrClassMap.h index de73afa9..d1235fcd 100755 --- a/lib/inc/drogon/DrClassMap.h +++ b/lib/inc/drogon/DrClassMap.h @@ -14,14 +14,15 @@ #pragma once +#include #include #include #include #include - #include #include #include +#include namespace drogon { @@ -34,7 +35,22 @@ class DrClassMap static void registerClass(const std::string &className, const DrAllocFunc &func); static DrObjectBase *newObject(const std::string &className); static const std::shared_ptr &getSingleInstance(const std::string &className); + template + static std::shared_ptr getSingleInstance() + { + static_assert(internal::IsSubClass::value, "T must be a sub-class of DrObjectBase"); + return std::dynamic_pointer_cast(getSingleInstance(T::classTypeName())); + } + static void setSingleInstance(const std::shared_ptr &ins); static std::vector getAllClassName(); + static const std::string demangle(const char *mangled_name) + { + std::size_t len = 0; + int status = 0; + std::unique_ptr ptr( + __cxxabiv1::__cxa_demangle(mangled_name, nullptr, &len, &status), &std::free); + return ptr.get(); + } protected: static std::unordered_map &getMap(); diff --git a/lib/inc/drogon/DrObject.h b/lib/inc/drogon/DrObject.h index 32881308..69be472c 100755 --- a/lib/inc/drogon/DrObject.h +++ b/lib/inc/drogon/DrObject.h @@ -16,8 +16,9 @@ #include -#include #include +#include + namespace drogon { class DrObjectBase @@ -32,14 +33,6 @@ class DrObjectBase { return (className() == class_name); } - static const std::string demangle(const char *mangled_name) - { - std::size_t len = 0; - int status = 0; - std::unique_ptr ptr( - __cxxabiv1::__cxa_demangle(mangled_name, nullptr, &len, &status), &std::free); - return ptr.get(); - } virtual ~DrObjectBase() {} }; @@ -74,15 +67,24 @@ class DrObject : public virtual DrObjectBase { public: DrAllocator() + { + registerClass(); + } + const std::string &className() const + { + static std::string className = DrClassMap::demangle(typeid(T).name()); + return className; + } + template + typename std::enable_if::value, void>::type registerClass() { DrClassMap::registerClass(className(), []() -> DrObjectBase * { return new T; }); } - const std::string &className() const + template + typename std::enable_if::value, void>::type registerClass() { - static std::string className = demangle(typeid(T).name()); - return className; } }; diff --git a/lib/inc/drogon/DrTemplateBase.h b/lib/inc/drogon/DrTemplateBase.h index e8032ea4..215e5b14 100755 --- a/lib/inc/drogon/DrTemplateBase.h +++ b/lib/inc/drogon/DrTemplateBase.h @@ -21,6 +21,7 @@ namespace drogon { + typedef HttpViewData DrTemplateData; /// The templating engine class @@ -35,7 +36,7 @@ class DrTemplateBase : public virtual DrObjectBase /** * The @param templateName represents the name of the template file. * A template file is a description file with a special format. Its extension is - * usually .csp and can be omitted. The user should preprocess the template file + * usually .csp. The user should preprocess the template file * with the drogon_ctl tool to create c++ source files. */ static std::shared_ptr newTemplate(std::string templateName); @@ -49,4 +50,5 @@ class DrTemplateBase : public virtual DrObjectBase virtual ~DrTemplateBase(){}; DrTemplateBase(){}; }; + } // namespace drogon diff --git a/lib/inc/drogon/HttpAppFramework.h b/lib/inc/drogon/HttpAppFramework.h index f7a1358b..28dd77a0 100755 --- a/lib/inc/drogon/HttpAppFramework.h +++ b/lib/inc/drogon/HttpAppFramework.h @@ -19,6 +19,7 @@ #include #endif #include +#include #include #include #include @@ -58,6 +59,10 @@ inline std::string getGitCommit() return VERSION_MD5; } +class HttpControllerBase; +class HttpSimpleControllerBase; +class WebSocketControllerBase; + class HttpAppFramework : public trantor::NonCopyable { public: @@ -115,7 +120,7 @@ class HttpAppFramework : public trantor::NonCopyable { static_assert(IsPlugin::value, "The Template parameter must be a subclass of PluginBase"); assert(isRunning()); - return dynamic_cast(getPlugin(T::className())); + return dynamic_cast(getPlugin(T::classTypeName())); } ///Get the plugin object registered in the framework @@ -216,6 +221,54 @@ class HttpAppFramework : public trantor::NonCopyable const std::vector &filters = std::vector()) = 0; + /// Register controller objects created and initialized by the user + /** + * Drogon can only automatically create controllers using the default constructor. + * Sometimes users want to be able to create controllers using constructors with + * parameters. Controllers created by user in this way should be registered to the framework + * via this method. + * The macro or configuration file is still valid for the path routing configuration + * of the controller created by users. + * + * NOTE: + * The declaration of the controller class must be as follows: + * class ApiTest : public drogon::HttpController + * { + * public: + * ApiTest(const std::string &str); + * ... + * }; + * The second template parameter must be explicitly set to false to disable automatic creation. + * And then user can create and register it somewhere as follows: + * auto ctrlPtr=std::make_shared("hello world"); + * drogon::app().registerController(ctrlPtr); + * This method should be called before calling the app().run() method. + */ + template + void registerController(const std::shared_ptr &ctrlPtr) + { + static_assert(internal::IsSubClass::value || + internal::IsSubClass::value || + internal::IsSubClass::value, + "Error! Only controller objects can be registered here"); + static_assert(!T::isAutoCreation, "Controllers created and initialized automatically by drogon cannot be registered here"); + DrClassMap::setSingleInstance(ctrlPtr); + T::initPathRouting(); + } + + /// Register filter objects created and initialized by the user + /** + * This method is similar to the above method. + */ + template + void registerFilter(const std::shared_ptr &filterPtr) + { + static_assert(internal::IsSubClass::value, + "Error! Only fitler objects can be registered here"); + static_assert(!T::isAutoCreation, "Filters created and initialized automatically by drogon cannot be registered here"); + DrClassMap::setSingleInstance(filterPtr); + } + ///Get the custom configuration defined by users in the configuration file. virtual const Json::Value &getCustomConfig() const = 0; diff --git a/lib/inc/drogon/HttpBinder.h b/lib/inc/drogon/HttpBinder.h index d9985955..e6a1c4db 100755 --- a/lib/inc/drogon/HttpBinder.h +++ b/lib/inc/drogon/HttpBinder.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -109,15 +110,13 @@ class HttpBinder : public HttpBinderBase FUNCTION _func; typedef FunctionTraits traits; - template < - std::size_t Index> + template using nth_argument_type = typename traits::template argument; static const size_t argument_count = traits::arity; - template < - typename... Values, - std::size_t Boundary = argument_count> + template typename std::enable_if<(sizeof...(Values) < Boundary), void>::type run( std::list &pathParameter, const HttpRequestPtr &req, std::function callback, @@ -152,14 +151,25 @@ class HttpBinder : public HttpBinderBase callFunction(req, callback, std::move(values)...); } template - typename std::enable_if::type callFunction( + bool isClassFunction = traits::isClassFunction, + bool isDrObjectClass = traits::isDrObjectClass> + typename std::enable_if::type callFunction( const HttpRequestPtr &req, std::function callback, Values &&... values) { - auto &obj = getControllerObj(); + static auto &obj = getControllerObj(); (obj.*_func)(req, callback, std::move(values)...); }; + template + typename std::enable_if::type callFunction( + const HttpRequestPtr &req, std::function callback, + Values &&... values) + { + static auto objPtr = DrClassMap::getSingleInstance(); + (*objPtr.*_func)(req, callback, std::move(values)...); + }; template typename std::enable_if::type callFunction( diff --git a/lib/inc/drogon/HttpController.h b/lib/inc/drogon/HttpController.h index e0075855..14ad8863 100755 --- a/lib/inc/drogon/HttpController.h +++ b/lib/inc/drogon/HttpController.h @@ -23,8 +23,8 @@ /// For more details on the class, see the wiki site (the 'HttpController' section) -#define METHOD_LIST_BEGIN \ - static void initMethods() \ +#define METHOD_LIST_BEGIN \ + static void initPathRouting() \ { #define METHOD_ADD(method, pattern, filters...) \ @@ -34,15 +34,21 @@ #define METHOD_LIST_END \ return; \ - } \ - \ - protected: + } namespace drogon { -template -class HttpController : public DrObject + +class HttpControllerBase { +}; + +template +class HttpController : public DrObject, public HttpControllerBase +{ + public: + static const bool isAutoCreation = AutoCreation; + protected: template static void registerMethod(FUNCTION &&function, @@ -60,12 +66,12 @@ class HttpController : public DrObject } if (pattern.empty() || pattern[0] == '/') app().registerHandler(path + pattern, - std::forward(function), - filtersAndMethods); + std::forward(function), + filtersAndMethods); else app().registerHandler(path + "/" + pattern, - std::forward(function), - filtersAndMethods); + std::forward(function), + filtersAndMethods); } private: @@ -74,7 +80,8 @@ class HttpController : public DrObject public: methodRegister() { - T::initMethods(); + if (AutoCreation) + T::initPathRouting(); } }; //use static value to register controller method in framework before main(); @@ -84,6 +91,6 @@ class HttpController : public DrObject return &_register; } }; -template -typename HttpController::methodRegister HttpController::_register; +template +typename HttpController::methodRegister HttpController::_register; } // namespace drogon diff --git a/lib/inc/drogon/HttpFilter.h b/lib/inc/drogon/HttpFilter.h index 024f76ba..04aa4615 100755 --- a/lib/inc/drogon/HttpFilter.h +++ b/lib/inc/drogon/HttpFilter.h @@ -33,10 +33,11 @@ class HttpFilterBase : public virtual DrObjectBase const FilterChainCallback &fccb) = 0; virtual ~HttpFilterBase() {} }; -template +template class HttpFilter : public DrObject, public HttpFilterBase { public: + static const bool isAutoCreation = AutoCreation; virtual ~HttpFilter() {} }; } // namespace drogon diff --git a/lib/inc/drogon/HttpSimpleController.h b/lib/inc/drogon/HttpSimpleController.h index 163bedc6..b37479c2 100755 --- a/lib/inc/drogon/HttpSimpleController.h +++ b/lib/inc/drogon/HttpSimpleController.h @@ -20,8 +20,8 @@ #include #include #include -#define PATH_LIST_BEGIN \ - static void ___paths___() \ +#define PATH_LIST_BEGIN \ + static void initPathRouting() \ { #define PATH_ADD(path, filters...) __registerSelf(path, {filters}); @@ -30,7 +30,7 @@ } namespace drogon { - + class HttpSimpleControllerBase : public virtual DrObjectBase { public: @@ -38,10 +38,11 @@ class HttpSimpleControllerBase : public virtual DrObjectBase virtual ~HttpSimpleControllerBase() {} }; -template +template class HttpSimpleController : public DrObject, public HttpSimpleControllerBase { public: + static const bool isAutoCreation = AutoCreation; virtual ~HttpSimpleController() {} protected: @@ -58,16 +59,11 @@ class HttpSimpleController : public DrObject, public HttpSimpleControllerBase public: pathRegister() { - T::___paths___(); + if (AutoCreation) + { + T::initPathRouting(); + } } - // protected: - // void _register(const std::string &className, const std::vector &paths) - // { - // for (auto const &reqPath : paths) - // { - // std::cout << "register controller class " << className << " on path " << reqPath << std::endl; - // } - // } }; friend pathRegister; static pathRegister _register; @@ -76,7 +72,7 @@ class HttpSimpleController : public DrObject, public HttpSimpleControllerBase return &_register; } }; -template -typename HttpSimpleController::pathRegister HttpSimpleController::_register; +template +typename HttpSimpleController::pathRegister HttpSimpleController::_register; } // namespace drogon diff --git a/lib/inc/drogon/WebSocketController.h b/lib/inc/drogon/WebSocketController.h index 2ee0f856..3900f3ec 100644 --- a/lib/inc/drogon/WebSocketController.h +++ b/lib/inc/drogon/WebSocketController.h @@ -23,9 +23,9 @@ #include #include #include -#define WS_PATH_LIST_BEGIN \ - static std::vector>> paths() \ - { \ +#define WS_PATH_LIST_BEGIN \ + static std::vector>> __paths() \ + { \ std::vector>> vet; #define WS_PATH_ADD(path, filters...) \ @@ -34,8 +34,10 @@ #define WS_PATH_LIST_END \ return vet; \ } + namespace drogon { + class WebSocketControllerBase : public virtual DrObjectBase { public: @@ -51,11 +53,24 @@ class WebSocketControllerBase : public virtual DrObjectBase }; typedef std::shared_ptr WebSocketControllerBasePtr; -template + +template class WebSocketController : public DrObject, public WebSocketControllerBase { public: + static const bool isAutoCreation = AutoCreation; virtual ~WebSocketController() {} + static void initPathRouting() + { + auto vPaths = T::__paths(); + for (auto const &path : vPaths) + { + LOG_TRACE << "register websocket controller (" << WebSocketController::classTypeName() << ") on path:" << path.first; + HttpAppFramework::instance().registerWebSocketController(path.first, + WebSocketController::classTypeName(), + path.second); + } + } protected: WebSocketController() {} @@ -66,25 +81,11 @@ class WebSocketController : public DrObject, public WebSocketControllerBase public: pathRegister() { - auto vPaths = T::paths(); - - for (auto const &path : vPaths) + if (AutoCreation) { - LOG_TRACE << "register websocket controller (" << WebSocketController::classTypeName() << ") on path:" << path.first; - HttpAppFramework::instance().registerWebSocketController(path.first, - WebSocketController::classTypeName(), - path.second); + T::initPathRouting(); } } - - // protected: - // void _register(const std::string &className, const std::vector &paths) - // { - // for (auto const &reqPath : paths) - // { - // std::cout << "register controller class " << className << " on path " << reqPath << std::endl; - // } - // } }; friend pathRegister; static pathRegister _register; @@ -93,7 +94,7 @@ class WebSocketController : public DrObject, public WebSocketControllerBase return &_register; } }; -template -typename WebSocketController::pathRegister WebSocketController::_register; +template +typename WebSocketController::pathRegister WebSocketController::_register; } // namespace drogon diff --git a/lib/inc/drogon/utils/ClassTraits.h b/lib/inc/drogon/utils/ClassTraits.h new file mode 100644 index 00000000..e499e86b --- /dev/null +++ b/lib/inc/drogon/utils/ClassTraits.h @@ -0,0 +1,34 @@ +/** + * + * ClassTraits.h + * An Tao + * + * Copyright 2018, An Tao. All rights reserved. + * https://github.com/an-tao/drogon + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Drogon + * + */ +#pragma once +#include + +namespace drogon +{ +namespace internal +{ + +/// This template is used to check whether S is a subclass of B. +template +struct IsSubClass +{ + typedef typename std::remove_cv::type>::type SubType; + typedef typename std::remove_cv::type>::type BaseType; + static char test(void *) { return 0; } + static int test(BaseType *) { return 0; } + static const bool value = (sizeof(test((SubType *)nullptr)) == sizeof(int)); +}; + +} // namespace internal +} // namespace drogon \ No newline at end of file diff --git a/lib/inc/drogon/utils/FunctionTraits.h b/lib/inc/drogon/utils/FunctionTraits.h index a54214cd..6f43b63c 100755 --- a/lib/inc/drogon/utils/FunctionTraits.h +++ b/lib/inc/drogon/utils/FunctionTraits.h @@ -14,6 +14,8 @@ #pragma once +#include +#include #include #include #include @@ -38,6 +40,7 @@ struct FunctionTraits : public FunctionTraits< decltype(&std::remove_reference::type::operator())> { static const bool isClassFunction = false; + static const bool isDrObjectClass = false; typedef void class_type; static const std::string name() { @@ -46,14 +49,13 @@ struct FunctionTraits : public FunctionTraits< }; //class instance method of const object -template < - typename ClassType, - typename ReturnType, - typename... Arguments> -struct FunctionTraits< - ReturnType (ClassType::*)(Arguments...) const> : FunctionTraits +template +struct FunctionTraits : FunctionTraits { static const bool isClassFunction = true; + static const bool isDrObjectClass = IsSubClass>::value; typedef ClassType class_type; static const std::string name() { return std::string("Class Function"); } }; @@ -63,10 +65,10 @@ template < typename ClassType, typename ReturnType, typename... Arguments> -struct FunctionTraits< - ReturnType (ClassType::*)(Arguments...)> : FunctionTraits +struct FunctionTraits : FunctionTraits { static const bool isClassFunction = true; + static const bool isDrObjectClass = IsSubClass>::value; typedef ClassType class_type; static const std::string name() { return std::string("Class Function"); } }; @@ -100,6 +102,7 @@ struct FunctionTraits< typedef void class_type; static const bool isHTTPFunction = false; static const bool isClassFunction = false; + static const bool isDrObjectClass = false; static const std::string name() { return std::string("Normal or Static Function"); } }; diff --git a/lib/src/DrClassMap.cc b/lib/src/DrClassMap.cc index 1d0475f2..ebe086cb 100755 --- a/lib/src/DrClassMap.cc +++ b/lib/src/DrClassMap.cc @@ -18,12 +18,31 @@ using namespace drogon; //std::map * DrClassMap::classMap=nullptr; //std::once_flag DrClassMap::flag; +namespace drogon +{ +namespace internal +{ + +static std::unordered_map> &getObjsMap() +{ + static std::unordered_map> singleInstanceMap; + return singleInstanceMap; +} + +static std::mutex &getMapMutex() +{ + static std::mutex mtx; + return mtx; +} + +} // namespace internal +} // namespace drogon + void DrClassMap::registerClass(const std::string &className, const DrAllocFunc &func) { - //std::cout<<"register class:"< &DrClassMap::getSingleInstance(const std::string &className) { - static std::unordered_map> singleInstanceMap; - static std::mutex mtx; + auto &mtx = internal::getMapMutex(); + auto &singleInstanceMap = internal::getObjsMap(); std::lock_guard lock(mtx); auto iter = singleInstanceMap.find(className); if (iter != singleInstanceMap.end()) @@ -45,6 +65,15 @@ const std::shared_ptr &DrClassMap::getSingleInstance(const std::st singleInstanceMap[className] = std::shared_ptr(newObject(className)); return singleInstanceMap[className]; } + +void DrClassMap::setSingleInstance(const std::shared_ptr &ins) +{ + auto &mtx = internal::getMapMutex(); + auto &singleInstanceMap = internal::getObjsMap(); + std::lock_guard lock(mtx); + singleInstanceMap[ins->className()] = ins; +} + std::vector DrClassMap::getAllClassName() { std::vector ret; @@ -54,6 +83,7 @@ std::vector DrClassMap::getAllClassName() } return ret; } + std::unordered_map &DrClassMap::getMap() { static std::unordered_map map; diff --git a/lib/src/WebsocketControllersRouter.cc b/lib/src/WebsocketControllersRouter.cc index 7e337876..4d776067 100644 --- a/lib/src/WebsocketControllersRouter.cc +++ b/lib/src/WebsocketControllersRouter.cc @@ -30,7 +30,7 @@ void WebsocketControllersRouter::registerWebSocketController(const std::string & assert(!ctrlName.empty()); std::string path(pathName); std::transform(pathName.begin(), pathName.end(), path.begin(), tolower); - auto objPtr = std::shared_ptr(DrClassMap::newObject(ctrlName)); + auto objPtr = DrClassMap::getSingleInstance(ctrlName); auto ctrlPtr = std::dynamic_pointer_cast(objPtr); assert(ctrlPtr); std::lock_guard guard(_websockCtrlMutex);