diff --git a/Makefile.am b/Makefile.am index 7a9d755b2..e15dae070 100644 --- a/Makefile.am +++ b/Makefile.am @@ -4,7 +4,7 @@ ACLOCAL_AMFLAGS = -I m4 $(ACLOCAL_MACOS_FLAGS) SUBDIRS = build m4 pixmaps po @ORTP_DIR@ @MS2_DIR@ \ - coreapi console gtk share scripts + coreapi console gtk share scripts tools diff --git a/configure.ac b/configure.ac index 97474be2e..1005ab2f5 100644 --- a/configure.ac +++ b/configure.ac @@ -140,6 +140,33 @@ AC_ARG_ENABLE(console_ui, *) AC_MSG_ERROR(bad value ${enableval} for --enable-console_ui) ;; esac],[console_ui=true]) +dnl conditionnal build of tools. +AC_ARG_ENABLE(tools, + [AS_HELP_STRING([--enable-tools=[yes/no]], [Turn on or off compilation of console interface (default=yes)])], + [case "${enableval}" in + yes) build_tools=true ;; + no) build_tools=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-tools) ;; + esac],[build_tools=check]) + +dnl check libxml2 (needed for tools) +if test "$build_tools" != "false" ; then + PKG_CHECK_MODULES(LIBXML2, [libxml-2.0],[], + [ + if test "$build_tools" = "true" ; then + AC_MSG_ERROR([Could not found libxml2, tools cannot be compiled.]) + else + build_tools=false + fi + ]) +fi + +AM_CONDITIONAL(BUILD_TOOLS, test x$build_tools != xfalse) +if test "$build_tools" != "false" ; then + build_tools=true + AC_DEFINE(BUILD_TOOLS, 1, [Define if tools enabled] ) +fi + dnl conditionnal build of gtk interface. AC_ARG_ENABLE(gtk_ui, [AS_HELP_STRING([--enable-gtk_ui=[yes/no]], [Turn on or off compilation of gtk interface (default=yes)])], @@ -418,8 +445,10 @@ AC_ARG_ENABLE(portaudio, dnl build console if required AM_CONDITIONAL(BUILD_CONSOLE, test x$console_ui = xtrue) + dnl special things for arm-linux cross compilation toolchain AM_CONDITIONAL(ARMBUILD, test x$use_arm_toolchain = xyes) + dnl compilation of gtk user interface AM_CONDITIONAL(BUILD_GTK_UI, [test x$gtk_ui = xtrue ] ) AM_CONDITIONAL(BUILD_WIN32, test x$mingw_found = xyes ) @@ -642,9 +671,11 @@ share/fr/Makefile share/it/Makefile share/ja/Makefile share/cs/Makefile +share/xml/Makefile share/linphone.pc share/linphone.desktop scripts/Makefile +tools/Makefile linphone.spec linphone.iss ]) @@ -657,6 +688,7 @@ printf "* Video support\t\t\t%s\n" $video printf "* GTK interface\t\t\t%s\n" $gtk_ui printf "* Account assistant\t\t%s\n" $build_wizard printf "* Console interface\t\t%s\n" $console_ui +printf "* Tools\t\t\t\t%s\n" $build_tools printf "* zRTP encryption (GPLv3)\t%s\n" $zrtp if test "$enable_tunnel" = "true" ; then diff --git a/coreapi/lpconfig.h b/coreapi/lpconfig.h index 8f3cae0a1..8e6f92128 100644 --- a/coreapi/lpconfig.h +++ b/coreapi/lpconfig.h @@ -25,6 +25,8 @@ #ifndef LPCONFIG_H #define LPCONFIG_H +#include + /** * The LpConfig object is used to manipulate a configuration file. * diff --git a/share/Makefile.am b/share/Makefile.am index 2f551f53d..83c3a934e 100644 --- a/share/Makefile.am +++ b/share/Makefile.am @@ -1,5 +1,5 @@ -SUBDIRS=C fr it ja cs +SUBDIRS=C fr it ja cs xml LINPHONE_SOUNDS=ringback.wav hello8000.wav hello16000.wav LINPHONE_RINGS=rings/orig.wav \ diff --git a/share/xml/Makefile.am b/share/xml/Makefile.am new file mode 100644 index 000000000..c553a5c76 --- /dev/null +++ b/share/xml/Makefile.am @@ -0,0 +1 @@ +EXTRA_DIST=lpconfig.xsd diff --git a/share/xml/lpconfig.xsd b/share/xml/lpconfig.xsd new file mode 100644 index 000000000..49bb56180 --- /dev/null +++ b/share/xml/lpconfig.xsd @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/Makefile.am b/tools/Makefile.am new file mode 100644 index 000000000..0c9f7e53b --- /dev/null +++ b/tools/Makefile.am @@ -0,0 +1,31 @@ +## Process this file with automake to produce Makefile.in + +AM_CPPFLAGS=\ + -I$(top_srcdir) \ + -I$(top_srcdir)/coreapi \ + -I$(top_srcdir)/exosip + +COMMON_CFLAGS=\ + -DIN_LINPHONE \ + $(LIBXML2_CFLAGS) \ + $(ORTP_CFLAGS) \ + $(STRICT_OPTIONS) + +if BUILD_TOOLS + +lib_LTLIBRARIES=libxml2lpc.la + +libxml2lpc_la_SOURCES=xml2lpc.c xml2lpc.h +libxml2lpc_la_CFLAGS=$(COMMON_CFLAGS) +libxml2lpc_la_LIBADD=\ + $(LIBXML2_LIBS) + +bin_PROGRAMS=xml2lpc_test + +xml2lpc_test_SOURCES=xml2lpc_test.c +xml2lpc_test_CFLAGS=$(COMMON_CFLAGS) +xml2lpc_test_LDADD=$(top_builddir)/coreapi/liblinphone.la libxml2lpc.la + +endif + + diff --git a/tools/xml2lpc.c b/tools/xml2lpc.c new file mode 100644 index 000000000..ce12eeb46 --- /dev/null +++ b/tools/xml2lpc.c @@ -0,0 +1,341 @@ +/* +linphone +Copyright (C) 2012 Belledonne Communications SARL +Yann DIORCET (yann.diorcet@linphone.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "xml2lpc.h" +#include +#include + + +#define XML2LPC_BZ 2048 + +struct _xml2lpc_context { + LpConfig *lpc; + xml2lpc_function cbf; + void *ctx; + + xmlDoc *doc; + xmlDoc *xsd; + char errorBuffer[XML2LPC_BZ]; + char warningBuffer[XML2LPC_BZ]; +}; + + +xml2lpc_context* xml2lpc_context_new(xml2lpc_function cbf, void *ctx) { + xml2lpc_context *xmlCtx = (xml2lpc_context*)malloc(sizeof(xml2lpc_context)); + if(xmlCtx != NULL) { + xmlCtx->lpc = NULL; + xmlCtx->cbf = cbf; + xmlCtx->ctx = ctx; + + xmlCtx->doc = NULL; + xmlCtx->xsd = NULL; + xmlCtx->errorBuffer[0]='\0'; + xmlCtx->warningBuffer[0]='\0'; + } + return xmlCtx; +} + +void xml2lpc_context_destroy(xml2lpc_context *ctx) { + if(ctx->doc != NULL) { + xmlFreeDoc(ctx->doc); + ctx->doc = NULL; + } + if(ctx->xsd != NULL) { + xmlFreeDoc(ctx->xsd); + ctx->xsd = NULL; + } + free(ctx); +} + +void xml2lpc_context_clear_logs(xml2lpc_context *ctx) { + ctx->errorBuffer[0]='\0'; + ctx->warningBuffer[0]='\0'; +} + +void xml2lpc_log(xml2lpc_context *xmlCtx, int level, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + if(xmlCtx->cbf != NULL) { + xmlCtx->cbf((xmlCtx)->ctx, level, fmt, args); + } + va_end(args); +} + +void xml2lpc_genericxml_error(void *ctx, const char *fmt, ...) { + xml2lpc_context *xmlCtx = (xml2lpc_context *)ctx; + int sl = strlen(xmlCtx->errorBuffer); + va_list args; + va_start(args, fmt); + vsnprintf(xmlCtx->errorBuffer + sl, XML2LPC_BZ-sl, fmt, args); + va_end(args); +} + +void xml2lpc_genericxml_warning(void *ctx, const char *fmt, ...) { + xml2lpc_context *xmlCtx = (xml2lpc_context *)ctx; + int sl = strlen(xmlCtx->warningBuffer); + va_list args; + va_start(args, fmt); + vsnprintf(xmlCtx->warningBuffer + sl, XML2LPC_BZ-sl, fmt, args); + va_end(args); +} + +static void dumpNodes(int level, xmlNode * a_node, xml2lpc_context *ctx) { + xmlNode *cur_node = NULL; + + for (cur_node = a_node; cur_node; cur_node = cur_node->next) { + if (cur_node->type == XML_ELEMENT_NODE) { + xml2lpc_log(ctx, XML2LPC_DEBUG, "node level: %d type: Element, name: %s", level, cur_node->name); + } else { + xml2lpc_log(ctx, XML2LPC_DEBUG, "node level: %d type: %d, name: %s", level, cur_node->type, cur_node->name); + } + + dumpNodes(level + 1, cur_node->children, ctx); + } +} + + +static void dumpNode(xmlNode *node, xml2lpc_context *ctx) { + xml2lpc_log(ctx, XML2LPC_DEBUG, "node type: %d, name: %s", node->type, node->name); +} + +static void dumpAttr(xmlNode *node, xml2lpc_context *ctx) { + xml2lpc_log(ctx, XML2LPC_DEBUG, "attr name: %s value:%s", node->name, node->children->content); +} + +static void dumpContent(xmlNode *node, xml2lpc_context *ctx) { + xml2lpc_log(ctx, XML2LPC_DEBUG, "content: %s", node->children->content); +} + +static int processEntry(xmlElement *element, const char *sectionName, xml2lpc_context *ctx) { + xmlNode *cur_attr = NULL; + const char *name = NULL; + const char *value = NULL; + bool_t overwrite = FALSE; + + for (cur_attr = (xmlNode *)element->attributes; cur_attr; cur_attr = cur_attr->next) { + dumpAttr(cur_attr, ctx); + if(strcmp((const char*)cur_attr->name, "name") == 0) { + name = (const char*)cur_attr->children->content; + } else if(strcmp((const char*)cur_attr->name, "overwrite") == 0) { + if(strcmp((const char*)cur_attr->children->content, "true") == 0) { + overwrite = TRUE; + } + } + } + + value = (const char *)element->children->content; + dumpContent((xmlNode *)element, ctx); + + if(name != NULL) { + const char *str = lp_config_get_string(ctx->lpc, sectionName, name, NULL); + if(str == NULL || overwrite) { + xml2lpc_log(ctx, XML2LPC_MESSAGE, "Set %s|%s = %s",sectionName, name, value); + lp_config_set_string(ctx->lpc, sectionName, name, value); + } else { + xml2lpc_log(ctx, XML2LPC_MESSAGE, "Don't touch %s|%s = %s",sectionName, name, str); + } + } else { + xml2lpc_log(ctx, XML2LPC_WARNING, "ignored entry with no \"name\" attribute line:%d",xmlGetLineNo((xmlNode*)element)); + } + return 0; +} + +static int processSection(xmlElement *element, xml2lpc_context *ctx) { + xmlNode *cur_node = NULL; + xmlNode *cur_attr = NULL; + const char *name = NULL; + + for (cur_attr = (xmlNode *)element->attributes; cur_attr; cur_attr = cur_attr->next) { + dumpAttr(cur_attr, ctx); + if(strcmp((const char*)cur_attr->name, "name") == 0) { + name = (const char*)cur_attr->children->content; + } + } + + if(name != NULL) { + for (cur_node = element->children; cur_node; cur_node = cur_node->next) { + dumpNode(cur_node, ctx); + if (cur_node->type == XML_ELEMENT_NODE) { + if(strcmp((const char*)cur_node->name, "entry") == 0 ) { + processEntry((xmlElement*)cur_node, name, ctx); + } + } + + } + } else { + xml2lpc_log(ctx, XML2LPC_WARNING, "ignored section with no \"name\" attribute, line:%d", xmlGetLineNo((xmlNode*)element)); + } + + return 0; +} + +static int processConfig(xmlElement *element, xml2lpc_context *ctx) { + xmlNode *cur_node = NULL; + + for (cur_node = element->children; cur_node; cur_node = cur_node->next) { + dumpNode(cur_node, ctx); + if (cur_node->type == XML_ELEMENT_NODE && + strcmp((const char*)cur_node->name, "section") == 0 ) { + processSection((xmlElement*)cur_node, ctx); + } + + } + return 0; +} + +static int processDoc(xmlNode *node, xml2lpc_context *ctx) { + dumpNode(node, ctx); + + if (node->type == XML_ELEMENT_NODE && + strcmp((const char*)node->name, "config") == 0 ) { + processConfig((xmlElement*)node, ctx); + } else { + xml2lpc_log(ctx, XML2LPC_WARNING, "root element is not \"config\", line:%d", xmlGetLineNo(node)); + } + return 0; +} + +static int internal_convert_xml2lpc(xmlDoc *doc, xml2lpc_context *ctx) { + xml2lpc_log(ctx, XML2LPC_DEBUG, "Parse started"); + xmlNode *rootNode = xmlDocGetRootElement(doc); + //dumpNodes(0, rootNode, cbf, ctx); + int ret = processDoc(rootNode, ctx); + xml2lpc_log(ctx, XML2LPC_DEBUG, "Parse ended ret:%d", ret); + return ret; +} + +int xml2lpc_validate(xml2lpc_context *xmlCtx) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSchemaValidCtxtPtr validCtx; + xmlSchemaParserCtxtPtr parserCtx = xmlSchemaNewDocParserCtxt(xmlCtx->xsd); + validCtx = xmlSchemaNewValidCtxt(xmlSchemaParse(parserCtx)); + xmlSchemaSetValidErrors(validCtx, xml2lpc_genericxml_error, xml2lpc_genericxml_warning, xmlCtx); + int ret = xmlSchemaValidateDoc(validCtx, xmlCtx->doc); + if(ret >0) { + xml2lpc_log(xmlCtx, XML2LPC_WARNING, "%s", xmlCtx->warningBuffer); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + } else { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Internal error"); + } + xmlSchemaFreeValidCtxt(validCtx); + return ret; +} + +int xml2lpc_convert(xml2lpc_context *xmlCtx, LpConfig *lpc) { + xml2lpc_context_clear_logs(xmlCtx); + xmlCtx->lpc = lpc; + return internal_convert_xml2lpc(xmlCtx->doc, xmlCtx); +} + +int xml2lpc_set_xml_file(xml2lpc_context* xmlCtx, const char *filename) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error); + if(xmlCtx->doc != NULL) { + xmlFreeDoc(xmlCtx->doc); + xmlCtx->doc = NULL; + } + xmlCtx->doc = xmlReadFile(filename, NULL, 0); + if(xmlCtx->doc == NULL) { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse file \"%s\"", filename); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + return -1; + } + return 0; +} + +int xml2lpc_set_xml_fd(xml2lpc_context* xmlCtx, int fd) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error); + if(xmlCtx->doc != NULL) { + xmlFreeDoc(xmlCtx->doc); + xmlCtx->doc = NULL; + } + xmlCtx->doc = xmlReadFd(fd, 0, NULL, 0); + if(xmlCtx->doc == NULL) { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse fd \"%d\"", fd); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + return -1; + } + return 0; +} + +int xml2lpc_set_xml_string(xml2lpc_context* xmlCtx, const char *content) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error); + if(xmlCtx->doc != NULL) { + xmlFreeDoc(xmlCtx->doc); + xmlCtx->doc = NULL; + } + xmlCtx->doc = xmlReadDoc((const unsigned char*)content, 0, NULL, 0); + if(xmlCtx->doc == NULL) { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't parse string"); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + return -1; + } + return 0; +} + +int xml2lpc_set_xsd_file(xml2lpc_context* xmlCtx, const char *filename) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error); + if(xmlCtx->xsd != NULL) { + xmlFreeDoc(xmlCtx->xsd); + xmlCtx->xsd = NULL; + } + xmlCtx->xsd = xmlReadFile(filename, NULL, 0); + if(xmlCtx->xsd == NULL) { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse file \"%s\"", filename); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + return -1; + } + return 0; +} + +int xml2lpc_set_xsd_fd(xml2lpc_context* xmlCtx, int fd) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error); + if(xmlCtx->xsd != NULL) { + xmlFreeDoc(xmlCtx->xsd); + xmlCtx->xsd = NULL; + } + xmlCtx->xsd = xmlReadFd(fd, 0, NULL, 0); + if(xmlCtx->xsd == NULL) { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse fd \"%d\"", fd); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + return -1; + } + return 0; +} + +int xml2lpc_set_xsd_string(xml2lpc_context* xmlCtx, const char *content) { + xml2lpc_context_clear_logs(xmlCtx); + xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error); + if(xmlCtx->xsd != NULL) { + xmlFreeDoc(xmlCtx->xsd); + xmlCtx->xsd = NULL; + } + xmlCtx->xsd = xmlReadDoc((const unsigned char*)content, 0, NULL, 0); + if(xmlCtx->xsd == NULL) { + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't parse string"); + xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer); + return -1; + } + return 0; +} \ No newline at end of file diff --git a/tools/xml2lpc.h b/tools/xml2lpc.h new file mode 100644 index 000000000..746fb0200 --- /dev/null +++ b/tools/xml2lpc.h @@ -0,0 +1,52 @@ +/* +linphone +Copyright (C) 2012 Belledonne Communications SARL + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#ifndef XML2LPC_H_ +#define XML2LPC_H_ + +#include "lpconfig.h" + +typedef struct _xml2lpc_context xml2lpc_context; + +typedef enum _xml2lpc_log_level { + XML2LPC_DEBUG = 0, + XML2LPC_MESSAGE, + XML2LPC_WARNING, + XML2LPC_ERROR +} xml2lpc_log_level; + +typedef void(*xml2lpc_function)(void *ctx, xml2lpc_log_level level, const char *fmt, va_list list); + +xml2lpc_context* xml2lpc_context_new(xml2lpc_function cbf, void *ctx); +void xml2lpc_context_destroy(xml2lpc_context*); + +int xml2lpc_set_xml_file(xml2lpc_context* context, const char *filename); +int xml2lpc_set_xml_fd(xml2lpc_context* context, int fd); +int xml2lpc_set_xml_string(xml2lpc_context* context, const char *content); + +int xml2lpc_set_xsd_file(xml2lpc_context* context, const char *filename); +int xml2lpc_set_xsd_fd(xml2lpc_context* context, int fd); +int xml2lpc_set_xsd_string(xml2lpc_context* context, const char *content); + +int xml2lpc_validate(xml2lpc_context *context); +int xml2lpc_convert(xml2lpc_context *context, LpConfig *lpc); + + + +#endif //XML2LPC_H_ diff --git a/tools/xml2lpc_test.c b/tools/xml2lpc_test.c new file mode 100644 index 000000000..cac2965ef --- /dev/null +++ b/tools/xml2lpc_test.c @@ -0,0 +1,72 @@ +/* +linphone +Copyright (C) 2012 Belledonne Communications SARL + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include +#include "xml2lpc.h" + +void cb_function(void *ctx, xml2lpc_log_level level, const char *msg, va_list list) { + const char *header; + switch(level) { + case XML2LPC_DEBUG: + header = "DEBUG"; + break; + case XML2LPC_MESSAGE: + header = "MESSAGE"; + break; + case XML2LPC_WARNING: + header = "WARNING"; + break; + case XML2LPC_ERROR: + header = "ERROR"; + break; + } + fprintf(stdout, "%s - ", header); + vfprintf(stdout, msg, list); + fprintf(stdout, "\n"); +} + +void show_usage(int argc, char *argv[]) { + fprintf(stderr, "usage %s convert \n" + " %s validate \n", + argv[0], argv[0]); +} + +int main(int argc, char *argv[]) { + if(argc != 4) { + show_usage(argc, argv); + return -1; + } + + xml2lpc_context *ctx = xml2lpc_context_new(cb_function, NULL); + xml2lpc_set_xml_file(ctx, argv[2]); + if(strcmp("convert", argv[1]) == 0) { + LpConfig *lpc = lp_config_new(argv[3]); + xml2lpc_convert(ctx, lpc); + lp_config_sync(lpc); + lp_config_destroy(lpc); + } else if(strcmp("validate", argv[1]) == 0) { + xml2lpc_set_xsd_file(ctx, argv[3]); + xml2lpc_validate(ctx); + } else { + show_usage(argc, argv); + } + xml2lpc_context_destroy(ctx); + return 0; +} +