537 lines
16 KiB
C
537 lines
16 KiB
C
/*
|
|
* FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
|
|
* Copyright (C) 2005-2012, Anthony Minessale II <anthm@freeswitch.org>
|
|
*
|
|
* Version: MPL 1.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (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.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Anthony Minessale II <anthm@freeswitch.org>
|
|
* Portions created by the Initial Developer are Copyright (C)
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Based on mod_skel by
|
|
* Anthony Minessale II <anthm@freeswitch.org>
|
|
*
|
|
* Contributor(s):
|
|
*
|
|
* Daniel Bryars <danb@aeriandi.com>
|
|
* Tim Brown <tim.brown@aeriandi.com>
|
|
* Anthony Minessale II <anthm@freeswitch.org>
|
|
* William King <william.king@quentustech.com>
|
|
* Mike Jerris <mike@jerris.com>
|
|
*
|
|
* kazoo.c -- Sends FreeSWITCH events to an AMQP broker
|
|
*
|
|
*/
|
|
|
|
#include "mod_kazoo.h"
|
|
|
|
static const char *LOG_LEVEL_NAMES[] = {
|
|
"SWITCH_LOG_DEBUG10",
|
|
"SWITCH_LOG_DEBUG9",
|
|
"SWITCH_LOG_DEBUG8",
|
|
"SWITCH_LOG_DEBUG7",
|
|
"SWITCH_LOG_DEBUG6",
|
|
"SWITCH_LOG_DEBUG5",
|
|
"SWITCH_LOG_DEBUG4",
|
|
"SWITCH_LOG_DEBUG3",
|
|
"SWITCH_LOG_DEBUG2",
|
|
"SWITCH_LOG_DEBUG1",
|
|
"SWITCH_LOG_DEBUG",
|
|
"SWITCH_LOG_INFO",
|
|
"SWITCH_LOG_NOTICE",
|
|
"SWITCH_LOG_WARNING",
|
|
"SWITCH_LOG_ERROR",
|
|
"SWITCH_LOG_CRIT",
|
|
"SWITCH_LOG_ALERT",
|
|
"SWITCH_LOG_CONSOLE",
|
|
"SWITCH_LOG_INVALID",
|
|
"SWITCH_LOG_UNINIT",
|
|
NULL
|
|
};
|
|
|
|
static const switch_log_level_t LOG_LEVEL_VALUES[] = {
|
|
SWITCH_LOG_DEBUG10,
|
|
SWITCH_LOG_DEBUG9,
|
|
SWITCH_LOG_DEBUG8,
|
|
SWITCH_LOG_DEBUG7,
|
|
SWITCH_LOG_DEBUG6,
|
|
SWITCH_LOG_DEBUG5,
|
|
SWITCH_LOG_DEBUG4,
|
|
SWITCH_LOG_DEBUG3,
|
|
SWITCH_LOG_DEBUG2,
|
|
SWITCH_LOG_DEBUG1,
|
|
SWITCH_LOG_DEBUG,
|
|
SWITCH_LOG_INFO,
|
|
SWITCH_LOG_NOTICE,
|
|
SWITCH_LOG_WARNING,
|
|
SWITCH_LOG_ERROR,
|
|
SWITCH_LOG_CRIT,
|
|
SWITCH_LOG_ALERT,
|
|
SWITCH_LOG_CONSOLE,
|
|
SWITCH_LOG_INVALID,
|
|
SWITCH_LOG_UNINIT
|
|
};
|
|
|
|
switch_log_level_t log_str2level(const char *str)
|
|
{
|
|
int x = 0;
|
|
switch_log_level_t level = SWITCH_LOG_INVALID;
|
|
|
|
if (switch_is_number(str)) {
|
|
x = atoi(str);
|
|
|
|
if (x > SWITCH_LOG_INVALID) {
|
|
return SWITCH_LOG_INVALID - 1;
|
|
} else if (x < 0) {
|
|
return 0;
|
|
} else {
|
|
return x;
|
|
}
|
|
}
|
|
|
|
|
|
for (x = 0;; x++) {
|
|
if (!LOG_LEVEL_NAMES[x]) {
|
|
break;
|
|
}
|
|
|
|
if (!strcasecmp(LOG_LEVEL_NAMES[x], str)) {
|
|
level = LOG_LEVEL_VALUES[x]; //(switch_log_level_t) x;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
switch_status_t kazoo_config_loglevels(switch_memory_pool_t *pool, switch_xml_t cfg, kazoo_loglevels_ptr *ptr)
|
|
{
|
|
switch_xml_t xml_level, xml_logging;
|
|
kazoo_loglevels_ptr loglevels = (kazoo_loglevels_ptr) switch_core_alloc(pool, sizeof(kazoo_loglevels_t));
|
|
|
|
loglevels->failed_log_level = SWITCH_LOG_ALERT;
|
|
loglevels->filtered_event_log_level = SWITCH_LOG_DEBUG1;
|
|
loglevels->filtered_field_log_level = SWITCH_LOG_DEBUG1;
|
|
loglevels->info_log_level = SWITCH_LOG_INFO;
|
|
loglevels->warn_log_level = SWITCH_LOG_WARNING;
|
|
loglevels->success_log_level = SWITCH_LOG_DEBUG;
|
|
loglevels->time_log_level = SWITCH_LOG_DEBUG1;
|
|
|
|
if ((xml_logging = switch_xml_child(cfg, "logging")) != NULL) {
|
|
for (xml_level = switch_xml_child(xml_logging, "log"); xml_level; xml_level = xml_level->next) {
|
|
char *var = (char *) switch_xml_attr_soft(xml_level, "name");
|
|
char *val = (char *) switch_xml_attr_soft(xml_level, "value");
|
|
|
|
if (!var) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "logging param missing 'name' attribute\n");
|
|
continue;
|
|
}
|
|
|
|
if (!val) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "logging param[%s] missing 'value' attribute\n", var);
|
|
continue;
|
|
}
|
|
|
|
if (!strncmp(var, "success", 7)) {
|
|
loglevels->success_log_level = log_str2level(val);
|
|
} else if (!strncmp(var, "failed", 6)) {
|
|
loglevels->failed_log_level = log_str2level(val);
|
|
} else if (!strncmp(var, "info", 4)) {
|
|
loglevels->info_log_level = log_str2level(val);
|
|
} else if (!strncmp(var, "warn", 4)) {
|
|
loglevels->warn_log_level = log_str2level(val);
|
|
} else if (!strncmp(var, "time", 4)) {
|
|
loglevels->time_log_level = log_str2level(val);
|
|
} else if (!strncmp(var, "filtered-event", 14)) {
|
|
loglevels->filtered_event_log_level = log_str2level(val);
|
|
} else if (!strncmp(var, "filtered-field", 14)) {
|
|
loglevels->filtered_field_log_level = log_str2level(val);
|
|
}
|
|
} /* xml_level for loop */
|
|
}
|
|
|
|
*ptr = loglevels;
|
|
return SWITCH_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
switch_status_t kazoo_config_filters(switch_memory_pool_t *pool, switch_xml_t cfg, kazoo_filter_ptr *ptr)
|
|
{
|
|
switch_xml_t filters, filter;
|
|
// char *routing_key = NULL;
|
|
kazoo_filter_ptr root = NULL, prv = NULL, cur = NULL;
|
|
|
|
|
|
if ((filters = switch_xml_child(cfg, "filters")) != NULL) {
|
|
for (filter = switch_xml_child(filters, "filter"); filter; filter = filter->next) {
|
|
const char *var = switch_xml_attr(filter, "name");
|
|
const char *val = switch_xml_attr(filter, "value");
|
|
const char *type = switch_xml_attr(filter, "type");
|
|
const char *compare = switch_xml_attr(filter, "compare");
|
|
cur = (kazoo_filter_ptr) switch_core_alloc(pool, sizeof(kazoo_filter));
|
|
memset(cur, 0, sizeof(kazoo_filter));
|
|
if(prv == NULL) {
|
|
root = prv = cur;
|
|
} else {
|
|
prv->next = cur;
|
|
prv = cur;
|
|
}
|
|
cur->type = FILTER_EXCLUDE;
|
|
cur->compare = FILTER_COMPARE_VALUE;
|
|
|
|
if(var)
|
|
cur->name = switch_core_strdup(pool, var);
|
|
|
|
if(val)
|
|
cur->value = switch_core_strdup(pool, val);
|
|
|
|
if(type) {
|
|
if (!strncmp(type, "exclude", 7)) {
|
|
cur->type = FILTER_EXCLUDE;
|
|
} else if (!strncmp(type, "include", 7)) {
|
|
cur->type = FILTER_INCLUDE;
|
|
}
|
|
}
|
|
|
|
if(compare) {
|
|
if (!strncmp(compare, "value", 7)) {
|
|
cur->compare = FILTER_COMPARE_VALUE;
|
|
} else if (!strncmp(compare, "prefix", 6)) {
|
|
cur->compare = FILTER_COMPARE_PREFIX;
|
|
} else if (!strncmp(compare, "list", 4)) {
|
|
cur->compare = FILTER_COMPARE_LIST;
|
|
} else if (!strncmp(compare, "exists", 6)) {
|
|
cur->compare = FILTER_COMPARE_EXISTS;
|
|
} else if (!strncmp(compare, "regex", 5)) {
|
|
cur->compare = FILTER_COMPARE_REGEX;
|
|
} else if (!strncmp(compare, "field", 5)) {
|
|
cur->compare = FILTER_COMPARE_FIELD;
|
|
}
|
|
}
|
|
|
|
if(cur->value == NULL)
|
|
cur->compare = FILTER_COMPARE_EXISTS;
|
|
|
|
if(cur->compare == FILTER_COMPARE_LIST) {
|
|
cur->list.size = switch_separate_string(cur->value, '|', cur->list.value, MAX_LIST_FIELDS);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
*ptr = root;
|
|
|
|
return SWITCH_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
switch_status_t kazoo_config_field(kazoo_config_ptr definitions, switch_memory_pool_t *pool, switch_xml_t cfg, kazoo_field_ptr *ptr)
|
|
{
|
|
const char *var = switch_xml_attr(cfg, "name");
|
|
const char *val = switch_xml_attr(cfg, "value");
|
|
const char *as = switch_xml_attr(cfg, "as");
|
|
const char *type = switch_xml_attr(cfg, "type");
|
|
const char *exclude_prefix = switch_xml_attr(cfg, "exclude-prefix");
|
|
const char *serialize_as = switch_xml_attr(cfg, "serialize-as");
|
|
kazoo_field_ptr cur = (kazoo_field_ptr) switch_core_alloc(pool, sizeof(kazoo_field));
|
|
cur->in_type = FIELD_NONE;
|
|
cur->out_type = JSON_NONE;
|
|
|
|
if(var)
|
|
cur->name = switch_core_strdup(pool, var);
|
|
|
|
if(val)
|
|
cur->value = switch_core_strdup(pool, val);
|
|
|
|
if(as)
|
|
cur->as = switch_core_strdup(pool, as);
|
|
|
|
if(type) {
|
|
if (!strncmp(type, "copy", 4)) {
|
|
cur->in_type = FIELD_COPY;
|
|
} else if (!strncmp(type, "static", 6)) {
|
|
cur->in_type = FIELD_STATIC;
|
|
} else if (!strncmp(type, "first-of", 8)) {
|
|
cur->in_type = FIELD_FIRST_OF;
|
|
} else if (!strncmp(type, "expand", 6)) {
|
|
cur->in_type = FIELD_EXPAND;
|
|
} else if (!strncmp(type, "prefix", 10)) {
|
|
cur->in_type = FIELD_PREFIX;
|
|
} else if (!strncmp(type, "group", 5)) {
|
|
cur->in_type = FIELD_GROUP;
|
|
} else if (!strncmp(type, "reference", 9)) {
|
|
cur->in_type = FIELD_REFERENCE;
|
|
}
|
|
}
|
|
|
|
if(serialize_as) {
|
|
if (!strncmp(serialize_as, "string", 5)) {
|
|
cur->out_type = JSON_STRING;
|
|
} else if (!strncmp(serialize_as, "number", 6)) {
|
|
cur->out_type = JSON_NUMBER;
|
|
} else if (!strncmp(serialize_as, "boolean", 7)) {
|
|
cur->out_type = JSON_BOOLEAN;
|
|
} else if (!strncmp(serialize_as, "object", 6)) {
|
|
cur->out_type = JSON_OBJECT;
|
|
} else if (!strncmp(serialize_as, "raw", 6)) {
|
|
cur->out_type = JSON_RAW;
|
|
}
|
|
}
|
|
|
|
if(exclude_prefix)
|
|
cur->exclude_prefix = switch_true(exclude_prefix);
|
|
|
|
kazoo_config_filters(pool, cfg, &cur->filter);
|
|
kazoo_config_fields(definitions, pool, cfg, &cur->children);
|
|
|
|
if(cur->children != NULL
|
|
&& (cur->in_type == FIELD_STATIC)
|
|
&& (cur->out_type == JSON_NONE)
|
|
) {
|
|
cur->out_type = JSON_OBJECT;
|
|
}
|
|
if(cur->in_type == FIELD_NONE) {
|
|
cur->in_type = FIELD_COPY;
|
|
}
|
|
|
|
if(cur->out_type == JSON_NONE) {
|
|
cur->out_type = JSON_STRING;
|
|
}
|
|
|
|
if(cur->in_type == FIELD_FIRST_OF) {
|
|
cur->list.size = switch_separate_string(cur->value, '|', cur->list.value, MAX_LIST_FIELDS);
|
|
}
|
|
|
|
if(cur->in_type == FIELD_REFERENCE) {
|
|
cur->ref = (kazoo_definition_ptr)switch_core_hash_find(definitions->hash, cur->name);
|
|
if(cur->ref == NULL) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "referenced field %s not found\n", cur->name);
|
|
}
|
|
}
|
|
|
|
*ptr = cur;
|
|
|
|
return SWITCH_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
switch_status_t kazoo_config_fields_loop(kazoo_config_ptr definitions, switch_memory_pool_t *pool, switch_xml_t cfg, kazoo_field_ptr *ptr)
|
|
{
|
|
switch_xml_t field;
|
|
kazoo_field_ptr root = NULL, prv = NULL;
|
|
|
|
|
|
for (field = switch_xml_child(cfg, "field"); field; field = field->next) {
|
|
kazoo_field_ptr cur = NULL;
|
|
kazoo_config_field(definitions, pool, field, &cur);
|
|
if(root == NULL) {
|
|
root = prv = cur;
|
|
} else {
|
|
prv->next = cur;
|
|
prv = cur;
|
|
}
|
|
}
|
|
|
|
*ptr = root;
|
|
|
|
return SWITCH_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
switch_status_t kazoo_config_fields(kazoo_config_ptr definitions, switch_memory_pool_t *pool, switch_xml_t cfg, kazoo_fields_ptr *ptr)
|
|
{
|
|
switch_xml_t fields;
|
|
kazoo_fields_ptr root = NULL;
|
|
|
|
|
|
if ((fields = switch_xml_child(cfg, "fields")) != NULL) {
|
|
const char *verbose = switch_xml_attr(fields, "verbose");
|
|
root = (kazoo_fields_ptr) switch_core_alloc(pool, sizeof(kazoo_fields));
|
|
root->verbose = SWITCH_TRUE;
|
|
if(verbose) {
|
|
root->verbose = switch_true(verbose);
|
|
}
|
|
|
|
kazoo_config_fields_loop(definitions, pool, fields, &root->head);
|
|
|
|
}
|
|
|
|
*ptr = root;
|
|
|
|
return SWITCH_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
kazoo_config_ptr kazoo_config_event_handlers(kazoo_config_ptr definitions, switch_xml_t cfg)
|
|
{
|
|
switch_xml_t xml_profiles = NULL, xml_profile = NULL;
|
|
kazoo_config_ptr profiles = NULL;
|
|
switch_memory_pool_t *pool = NULL;
|
|
|
|
if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "error creating memory pool for producers\n");
|
|
return NULL;
|
|
}
|
|
|
|
profiles = switch_core_alloc(pool, sizeof(kazoo_config));
|
|
profiles->pool = pool;
|
|
switch_core_hash_init(&profiles->hash);
|
|
|
|
if ((xml_profiles = switch_xml_child(cfg, "event-handlers"))) {
|
|
if ((xml_profile = switch_xml_child(xml_profiles, "profile"))) {
|
|
for (; xml_profile; xml_profile = xml_profile->next) {
|
|
const char *name = switch_xml_attr(xml_profile, "name");
|
|
if(name == NULL) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "missing attr name\n" );
|
|
continue;
|
|
}
|
|
kazoo_config_event_handler(definitions, profiles, xml_profile, NULL);
|
|
}
|
|
} else {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "unable to locate a event-handler profile for kazoo\n" );
|
|
}
|
|
} else {
|
|
destroy_config(&profiles);
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "unable to locate event-handlers section for kazoo, using default\n" );
|
|
}
|
|
|
|
return profiles;
|
|
|
|
}
|
|
|
|
kazoo_config_ptr kazoo_config_fetch_handlers(kazoo_config_ptr definitions, switch_xml_t cfg)
|
|
{
|
|
switch_xml_t xml_profiles = NULL, xml_profile = NULL;
|
|
kazoo_config_ptr profiles = NULL;
|
|
switch_memory_pool_t *pool = NULL;
|
|
|
|
if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "error creating memory pool for producers\n");
|
|
return NULL;
|
|
}
|
|
|
|
profiles = switch_core_alloc(pool, sizeof(kazoo_config));
|
|
profiles->pool = pool;
|
|
switch_core_hash_init(&profiles->hash);
|
|
|
|
if ((xml_profiles = switch_xml_child(cfg, "fetch-handlers"))) {
|
|
if ((xml_profile = switch_xml_child(xml_profiles, "profile"))) {
|
|
for (; xml_profile; xml_profile = xml_profile->next) {
|
|
const char *name = switch_xml_attr(xml_profile, "name");
|
|
if(name == NULL) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "missing attr name\n" );
|
|
continue;
|
|
}
|
|
kazoo_config_fetch_handler(definitions, profiles, xml_profile, NULL);
|
|
}
|
|
} else {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "unable to locate a fetch-handler profile for kazoo\n" );
|
|
}
|
|
} else {
|
|
destroy_config(&profiles);
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "unable to locate fetch-handlers section for kazoo, using default\n" );
|
|
}
|
|
|
|
return profiles;
|
|
|
|
}
|
|
|
|
|
|
switch_status_t kazoo_config_definition(kazoo_config_ptr root, switch_xml_t cfg)
|
|
{
|
|
kazoo_definition_ptr definition = NULL;
|
|
char *name = (char *) switch_xml_attr_soft(cfg, "name");
|
|
|
|
if (zstr(name)) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to load kazoo profile, check definition missing name attr\n");
|
|
return SWITCH_STATUS_GENERR;
|
|
}
|
|
|
|
definition = switch_core_alloc(root->pool, sizeof(kazoo_definition));
|
|
definition->name = switch_core_strdup(root->pool, name);
|
|
|
|
kazoo_config_filters(root->pool, cfg, &definition->filter);
|
|
kazoo_config_fields_loop(root, root->pool, cfg, &definition->head);
|
|
|
|
if ( switch_core_hash_insert(root->hash, name, (void *) definition) != SWITCH_STATUS_SUCCESS) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to insert new definition [%s] into kazoo definitions hash\n", name);
|
|
return SWITCH_STATUS_GENERR;
|
|
}
|
|
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "definition[%s] successfully configured\n", definition->name);
|
|
return SWITCH_STATUS_SUCCESS;
|
|
}
|
|
|
|
kazoo_config_ptr kazoo_config_definitions(switch_xml_t cfg)
|
|
{
|
|
switch_xml_t xml_definitions = NULL, xml_definition = NULL;
|
|
kazoo_config_ptr definitions = NULL;
|
|
switch_memory_pool_t *pool = NULL;
|
|
|
|
if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "error creating memory pool for definitions\n");
|
|
return NULL;
|
|
}
|
|
|
|
definitions = switch_core_alloc(pool, sizeof(kazoo_config));
|
|
definitions->pool = pool;
|
|
switch_core_hash_init(&definitions->hash);
|
|
|
|
if ((xml_definitions = switch_xml_child(cfg, "definitions"))) {
|
|
if ((xml_definition = switch_xml_child(xml_definitions, "definition"))) {
|
|
for (; xml_definition; xml_definition = xml_definition->next) {
|
|
kazoo_config_definition(definitions, xml_definition);
|
|
}
|
|
} else {
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "no definitions for kazoo\n" );
|
|
}
|
|
} else {
|
|
destroy_config(&definitions);
|
|
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "unable to locate definitions section for kazoo, using default\n" );
|
|
}
|
|
|
|
return definitions;
|
|
}
|
|
|
|
void destroy_config(kazoo_config_ptr *ptr)
|
|
{
|
|
kazoo_config_ptr config = NULL;
|
|
switch_memory_pool_t *pool;
|
|
|
|
if (!ptr || !*ptr) {
|
|
return;
|
|
}
|
|
config = *ptr;
|
|
pool = config->pool;
|
|
|
|
switch_core_hash_destroy(&(config->hash));
|
|
switch_core_destroy_memory_pool(&pool);
|
|
|
|
*ptr = NULL;
|
|
}
|
|
|
|
/* For Emacs:
|
|
* Local Variables:
|
|
* mode:c
|
|
* indent-tabs-mode:t
|
|
* tab-width:4
|
|
* c-basic-offset:4
|
|
* End:
|
|
* For VIM:
|
|
* vim:set softtabstop=4 shiftwidth=4 tabstop=4
|
|
*/
|