mirror of
https://github.com/MariaDB/server.git
synced 2025-01-17 04:22:27 +01:00
19af1890b5
This commit replaces sprintf(buf, ...) with snprintf(buf, sizeof(buf), ...), specifically in the "easy" cases where buf is allocated with a size known at compile time. The changes make sure we are not write outside array/string bounds which will lead to undefined behaviour. In case the code is trying to write outside bounds - safe version of functions simply cut the string messages so we process this gracefully. All new code of the whole pull request, including one or several files that are either new files or modified ones, are contributed under the BSD-new license. I am contributing on behalf of my employer Amazon Web Services, Inc. bsonudf.cpp warnings cleanup by Daniel Black Reviewer: Daniel Black
632 lines
18 KiB
C++
632 lines
18 KiB
C++
/************** tabjmg C++ Program Source Code File (.CPP) *************/
|
|
/* PROGRAM NAME: tabjmg Version 1.3 */
|
|
/* (C) Copyright to the author Olivier BERTRAND 2021 */
|
|
/* This file contains the MongoDB classes using the Java Driver. */
|
|
/***********************************************************************/
|
|
|
|
/***********************************************************************/
|
|
/* Include relevant sections of the MariaDB header file. */
|
|
/***********************************************************************/
|
|
#include <my_global.h>
|
|
|
|
/***********************************************************************/
|
|
/* Include application header files: */
|
|
/* global.h is header containing all global declarations. */
|
|
/* plgdbsem.h is header containing the DB application declarations. */
|
|
/***********************************************************************/
|
|
#include "global.h"
|
|
#include "plgdbsem.h"
|
|
#include "xtable.h"
|
|
#include "maputil.h"
|
|
#include "filamtxt.h"
|
|
#include "tabext.h"
|
|
#include "tabjmg.h"
|
|
#include "tabmul.h"
|
|
#include "checklvl.h"
|
|
#include "resource.h"
|
|
#include "mycat.h" // for FNC_COL
|
|
#include "filter.h"
|
|
|
|
#define nullptr 0
|
|
|
|
PQRYRES MGOColumns(PGLOBAL g, PCSZ db, PCSZ uri, PTOS topt, bool info);
|
|
bool Stringified(PCSZ, char*);
|
|
|
|
/* -------------------------- Class JMGDISC -------------------------- */
|
|
|
|
/***********************************************************************/
|
|
/* Constructor */
|
|
/***********************************************************************/
|
|
JMGDISC::JMGDISC(PGLOBAL g, int *lg) : MGODISC(g, lg)
|
|
{
|
|
drv = "Java"; Jcp = NULL; columnid = nullptr; bvnameid = nullptr;
|
|
} // end of JMGDISC constructor
|
|
|
|
/***********************************************************************/
|
|
/* Initialyze. */
|
|
/***********************************************************************/
|
|
bool JMGDISC::Init(PGLOBAL g)
|
|
{
|
|
if (!(Jcp = ((TDBJMG*)tmgp)->Jcp)) {
|
|
strcpy(g->Message, "Init: Jcp is NULL");
|
|
return true;
|
|
} else if (Jcp->gmID(g, columnid, "ColumnDesc",
|
|
"(Ljava/lang/Object;I[II)Ljava/lang/Object;"))
|
|
return true;
|
|
else if (Jcp->gmID(g, bvnameid, "ColDescName", "()Ljava/lang/String;"))
|
|
return true;
|
|
|
|
return false;
|
|
} // end of Init
|
|
|
|
/***********************************************************************/
|
|
/* Analyse passed document. */
|
|
/***********************************************************************/
|
|
bool JMGDISC::Find(PGLOBAL g)
|
|
{
|
|
return ColDesc(g, nullptr, NULL, NULL, Jcp->m_Ncol, 0);
|
|
} // end of Find
|
|
|
|
/***********************************************************************/
|
|
/* Analyse passed document. */
|
|
/***********************************************************************/
|
|
bool JMGDISC::ColDesc(PGLOBAL g, jobject obj, char *pcn, char *pfmt,
|
|
int ncol, int k)
|
|
{
|
|
const char *key, *utf;
|
|
char colname[65];
|
|
char fmt[129];
|
|
bool rc = true;
|
|
size_t z;
|
|
jint *n = nullptr;
|
|
jstring jkey;
|
|
jobject jres;
|
|
|
|
// Build the java int array
|
|
jintArray val = Jcp->env->NewIntArray(5);
|
|
|
|
if (val == nullptr) {
|
|
strcpy(g->Message, "Cannot allocate jint array");
|
|
return true;
|
|
} else if (!ncol)
|
|
n = Jcp->env->GetIntArrayElements(val, 0);
|
|
|
|
for (int i = 0; i < ncol; i++) {
|
|
jres = Jcp->env->CallObjectMethod(Jcp->job, columnid, obj, i, val, lvl - k);
|
|
n = Jcp->env->GetIntArrayElements(val, 0);
|
|
|
|
if (Jcp->Check(n[0])) {
|
|
snprintf(g->Message, sizeof(g->Message), "ColDesc: %s", Jcp->Msg);
|
|
goto err;
|
|
} else if (!n[0])
|
|
continue;
|
|
|
|
jkey = (jstring)Jcp->env->CallObjectMethod(Jcp->job, bvnameid);
|
|
utf = Jcp->env->GetStringUTFChars(jkey, nullptr);
|
|
key = PlugDup(g, utf);
|
|
Jcp->env->ReleaseStringUTFChars(jkey, utf);
|
|
Jcp->env->DeleteLocalRef(jkey);
|
|
|
|
if (pcn) {
|
|
strncpy(colname, pcn, 64);
|
|
colname[64] = 0;
|
|
z = 65 - strlen(colname);
|
|
strncat(strncat(colname, "_", z), key, z - 1);
|
|
} else
|
|
strcpy(colname, key);
|
|
|
|
if (pfmt) {
|
|
strncpy(fmt, pfmt, 128);
|
|
fmt[128] = 0;
|
|
z = 129 - strlen(fmt);
|
|
strncat(strncat(fmt, ".", z), key, z - 1);
|
|
} else
|
|
strcpy(fmt, key);
|
|
|
|
if (!jres) {
|
|
bcol.Type = n[0];
|
|
bcol.Len = n[1];
|
|
bcol.Scale = n[2];
|
|
bcol.Cbn = n[3];
|
|
AddColumn(g, colname, fmt, k);
|
|
} else {
|
|
if (n[0] == 2 && !all)
|
|
n[4] = MY_MIN(n[4], 1);
|
|
|
|
if (ColDesc(g, jres, colname, fmt, n[4], k + 1))
|
|
goto err;
|
|
|
|
} // endif jres
|
|
|
|
} // endfor i
|
|
|
|
rc = false;
|
|
|
|
err:
|
|
Jcp->env->ReleaseIntArrayElements(val, n, 0);
|
|
return rc;
|
|
} // end of ColDesc
|
|
|
|
/* --------------------------- Class TDBJMG -------------------------- */
|
|
|
|
/***********************************************************************/
|
|
/* Implementation of the TDBJMG class. */
|
|
/***********************************************************************/
|
|
TDBJMG::TDBJMG(PMGODEF tdp) : TDBEXT(tdp)
|
|
{
|
|
Jcp = NULL;
|
|
//Cnp = NULL;
|
|
|
|
if (tdp) {
|
|
Ops.Driver = tdp->Tabschema;
|
|
Ops.Url = tdp->Uri;
|
|
Ops.Version = tdp->Version;
|
|
Uri = tdp->Uri;
|
|
Db_name = tdp->Tabschema;
|
|
Wrapname = tdp->Wrapname;
|
|
Coll_name = tdp->Tabname;
|
|
Options = tdp->Colist;
|
|
Filter = tdp->Filter;
|
|
Strfy = tdp->Strfy;
|
|
B = tdp->Base ? 1 : 0;
|
|
Pipe = tdp->Pipe && Options != NULL;
|
|
} else {
|
|
Ops.Driver = NULL;
|
|
Ops.Url = NULL;
|
|
Ops.Version = 0;
|
|
Uri = NULL;
|
|
Db_name = NULL;
|
|
Coll_name = NULL;
|
|
Options = NULL;
|
|
Filter = NULL;
|
|
Strfy = NULL;
|
|
B = 0;
|
|
Pipe = false;
|
|
} // endif tdp
|
|
|
|
Ops.User = NULL;
|
|
Ops.Pwd = NULL;
|
|
Ops.Scrollable = false;
|
|
Ops.Fsize = 0;
|
|
Fpos = -1;
|
|
N = 0;
|
|
Done = false;
|
|
} // end of TDBJMG standard constructor
|
|
|
|
TDBJMG::TDBJMG(TDBJMG *tdbp) : TDBEXT(tdbp)
|
|
{
|
|
Uri = tdbp->Uri;
|
|
Db_name = tdbp->Db_name;;
|
|
Coll_name = tdbp->Coll_name;
|
|
Options = tdbp->Options;
|
|
Filter = tdbp->Filter;
|
|
Strfy = tdbp->Strfy;
|
|
B = tdbp->B;
|
|
Fpos = tdbp->Fpos;
|
|
N = tdbp->N;
|
|
Done = tdbp->Done;
|
|
Pipe = tdbp->Pipe;
|
|
} // end of TDBJMG copy constructor
|
|
|
|
// Used for update
|
|
PTDB TDBJMG::Clone(PTABS t)
|
|
{
|
|
PTDB tp;
|
|
PJMGCOL cp1, cp2;
|
|
PGLOBAL g = t->G;
|
|
|
|
tp = new(g) TDBJMG(this);
|
|
|
|
for (cp1 = (PJMGCOL)Columns; cp1; cp1 = (PJMGCOL)cp1->GetNext())
|
|
if (!cp1->IsSpecial()) {
|
|
cp2 = new(g) JMGCOL(cp1, tp); // Make a copy
|
|
NewPointer(t, cp1, cp2);
|
|
} // endif cp1
|
|
|
|
return tp;
|
|
} // end of Clone
|
|
|
|
/***********************************************************************/
|
|
/* Allocate JSN column description block. */
|
|
/***********************************************************************/
|
|
PCOL TDBJMG::MakeCol(PGLOBAL g, PCOLDEF cdp, PCOL cprec, int n)
|
|
{
|
|
return new(g) JMGCOL(g, cdp, this, cprec, n);
|
|
} // end of MakeCol
|
|
|
|
/***********************************************************************/
|
|
/* InsertSpecialColumn: Put a special column ahead of the column list.*/
|
|
/***********************************************************************/
|
|
PCOL TDBJMG::InsertSpecialColumn(PCOL colp)
|
|
{
|
|
if (!colp->IsSpecial())
|
|
return NULL;
|
|
|
|
colp->SetNext(Columns);
|
|
Columns = colp;
|
|
return colp;
|
|
} // end of InsertSpecialColumn
|
|
|
|
/***********************************************************************/
|
|
/* MONGO Cardinality: returns table size in number of rows. */
|
|
/***********************************************************************/
|
|
int TDBJMG::Cardinality(PGLOBAL g)
|
|
{
|
|
if (!g)
|
|
return 1;
|
|
else if (Cardinal < 0)
|
|
Cardinal = (!Init(g)) ? Jcp->CollSize(g) : 0;
|
|
|
|
return Cardinal;
|
|
} // end of Cardinality
|
|
|
|
/***********************************************************************/
|
|
/* MONGO GetMaxSize: returns collection size estimate. */
|
|
/***********************************************************************/
|
|
int TDBJMG::GetMaxSize(PGLOBAL g)
|
|
{
|
|
if (MaxSize < 0)
|
|
MaxSize = Cardinality(g);
|
|
|
|
return MaxSize;
|
|
} // end of GetMaxSize
|
|
|
|
/***********************************************************************/
|
|
/* Init: initialize MongoDB processing. */
|
|
/***********************************************************************/
|
|
bool TDBJMG::Init(PGLOBAL g)
|
|
{
|
|
if (Done)
|
|
return false;
|
|
|
|
/*********************************************************************/
|
|
/* Open an JDBC connection for this table. */
|
|
/* Note: this may not be the proper way to do. Perhaps it is better */
|
|
/* to test whether a connection is already open for this datasource */
|
|
/* and if so to allocate just a new result set. But this only for */
|
|
/* drivers allowing concurency in getting results ??? */
|
|
/*********************************************************************/
|
|
if (!Jcp)
|
|
Jcp = new(g) JMgoConn(g, Coll_name, Wrapname);
|
|
else if (Jcp->IsOpen())
|
|
Jcp->Close();
|
|
|
|
if (Jcp->Connect(&Ops))
|
|
return true;
|
|
|
|
Done = true;
|
|
return false;
|
|
} // end of Init
|
|
|
|
/***********************************************************************/
|
|
/* OpenDB: Data Base open routine for MONGO access method. */
|
|
/***********************************************************************/
|
|
bool TDBJMG::OpenDB(PGLOBAL g)
|
|
{
|
|
if (Use == USE_OPEN) {
|
|
/*******************************************************************/
|
|
/* Table already open replace it at its beginning. */
|
|
/*******************************************************************/
|
|
if (Jcp->Rewind())
|
|
return true;
|
|
|
|
Fpos = -1;
|
|
return false;
|
|
} // endif Use
|
|
|
|
/*********************************************************************/
|
|
/* First opening. */
|
|
/*********************************************************************/
|
|
if (Pipe && Mode != MODE_READ) {
|
|
strcpy(g->Message, "Pipeline tables are read only");
|
|
return true;
|
|
} // endif Pipe
|
|
|
|
Use = USE_OPEN; // Do it now in case we are recursively called
|
|
|
|
if (Init(g))
|
|
return true;
|
|
|
|
if (Jcp->GetMethodId(g, Mode))
|
|
return true;
|
|
|
|
if (Mode == MODE_DELETE && !Next) {
|
|
// Delete all documents
|
|
if (!Jcp->MakeCursor(g, this, "all", Filter, false))
|
|
if (Jcp->DocDelete(g, true) == RC_OK)
|
|
return false;
|
|
|
|
return true;
|
|
} // endif Mode
|
|
|
|
if (Mode == MODE_INSERT)
|
|
Jcp->MakeColumnGroups(g, this);
|
|
|
|
if (Mode != MODE_UPDATE)
|
|
return Jcp->MakeCursor(g, this, Options, Filter, Pipe);
|
|
|
|
return false;
|
|
} // end of OpenDB
|
|
|
|
/***********************************************************************/
|
|
/* Data Base indexed read routine for ODBC access method. */
|
|
/***********************************************************************/
|
|
bool TDBJMG::ReadKey(PGLOBAL g, OPVAL op, const key_range *kr)
|
|
{
|
|
strcpy(g->Message, "MONGO tables are not indexable");
|
|
return true;
|
|
} // end of ReadKey
|
|
|
|
/***********************************************************************/
|
|
/* ReadDB: Get next document from a collection. */
|
|
/***********************************************************************/
|
|
int TDBJMG::ReadDB(PGLOBAL g)
|
|
{
|
|
int rc = RC_OK;
|
|
|
|
if (!N && Mode == MODE_UPDATE)
|
|
if (Jcp->MakeCursor(g, this, Options, Filter, Pipe))
|
|
return RC_FX;
|
|
|
|
if (++CurNum >= Rbuf) {
|
|
Rbuf = Jcp->Fetch();
|
|
Curpos = Fpos + 1;
|
|
CurNum = 0;
|
|
N++;
|
|
} // endif CurNum
|
|
|
|
rc = (Rbuf > 0) ? RC_OK : (Rbuf == 0) ? RC_EF : RC_FX;
|
|
|
|
return rc;
|
|
} // end of ReadDB
|
|
|
|
/***********************************************************************/
|
|
/* WriteDB: Data Base write routine for DOS access method. */
|
|
/***********************************************************************/
|
|
int TDBJMG::WriteDB(PGLOBAL g)
|
|
{
|
|
int rc = RC_OK;
|
|
|
|
if (Mode == MODE_INSERT) {
|
|
rc = Jcp->DocWrite(g, NULL);
|
|
} else if (Mode == MODE_DELETE) {
|
|
rc = Jcp->DocDelete(g, false);
|
|
} else if (Mode == MODE_UPDATE) {
|
|
rc = Jcp->DocUpdate(g, this);
|
|
} // endif Mode
|
|
|
|
return rc;
|
|
} // end of WriteDB
|
|
|
|
/***********************************************************************/
|
|
/* Data Base delete line routine for ODBC access method. */
|
|
/***********************************************************************/
|
|
int TDBJMG::DeleteDB(PGLOBAL g, int irc)
|
|
{
|
|
return (irc == RC_OK) ? WriteDB(g) : RC_OK;
|
|
} // end of DeleteDB
|
|
|
|
/***********************************************************************/
|
|
/* Table close routine for MONGO tables. */
|
|
/***********************************************************************/
|
|
void TDBJMG::CloseDB(PGLOBAL g)
|
|
{
|
|
Jcp->Close();
|
|
Done = false;
|
|
} // end of CloseDB
|
|
|
|
/* ----------------------------- JMGCOL ------------------------------ */
|
|
|
|
/***********************************************************************/
|
|
/* JMGCOL public constructor. */
|
|
/***********************************************************************/
|
|
JMGCOL::JMGCOL(PGLOBAL g, PCOLDEF cdp, PTDB tdbp, PCOL cprec, int i)
|
|
: EXTCOL(cdp, tdbp, cprec, i, "MGO")
|
|
{
|
|
Tmgp = (PTDBJMG)(tdbp->GetOrig() ? tdbp->GetOrig() : tdbp);
|
|
Sgfy = Stringified(Tmgp->Strfy, Name);
|
|
|
|
if ((Jpath = cdp->GetFmt())) {
|
|
int n = strlen(Jpath);
|
|
|
|
if (n && Jpath[n - 1] == '*') {
|
|
Jpath = PlugDup(g, cdp->GetFmt());
|
|
|
|
if (--n) {
|
|
if (Jpath[n - 1] == '.') n--;
|
|
Jpath[n] = 0;
|
|
} // endif n
|
|
|
|
Sgfy = true;
|
|
} // endif Jpath
|
|
|
|
} else
|
|
Jpath = cdp->GetName();
|
|
|
|
} // end of JMGCOL constructor
|
|
|
|
/***********************************************************************/
|
|
/* JMGCOL constructor used for copying columns. */
|
|
/* tdbp is the pointer to the new table descriptor. */
|
|
/***********************************************************************/
|
|
JMGCOL::JMGCOL(JMGCOL *col1, PTDB tdbp) : EXTCOL(col1, tdbp)
|
|
{
|
|
Tmgp = col1->Tmgp;
|
|
Jpath = col1->Jpath;
|
|
Sgfy = col1->Sgfy;
|
|
} // end of JMGCOL copy constructor
|
|
|
|
/***********************************************************************/
|
|
/* Get path when proj is false or projection path when proj is true. */
|
|
/***********************************************************************/
|
|
PSZ JMGCOL::GetJpath(PGLOBAL g, bool proj)
|
|
{
|
|
if (Jpath) {
|
|
if (proj) {
|
|
char* p1, * p2, * projpath = PlugDup(g, Jpath);
|
|
int i = 0;
|
|
|
|
for (p1 = p2 = projpath; *p1; p1++)
|
|
if (*p1 == '.') {
|
|
if (!i)
|
|
*p2++ = *p1;
|
|
|
|
i = 1;
|
|
} else if (i) {
|
|
if (!isdigit(*p1)) {
|
|
*p2++ = *p1;
|
|
i = 0;
|
|
} // endif p1
|
|
|
|
} else
|
|
*p2++ = *p1;
|
|
|
|
if (*(p2 - 1) == '.')
|
|
p2--;
|
|
|
|
*p2 = 0;
|
|
return projpath;
|
|
} else
|
|
return Jpath;
|
|
|
|
} else
|
|
return Name;
|
|
|
|
} // end of GetJpath
|
|
|
|
#if 0
|
|
/***********************************************************************/
|
|
/* Mini: used to suppress blanks to json strings. */
|
|
/***********************************************************************/
|
|
char *JMGCOL::Mini(PGLOBAL g, const bson_t *bson, bool b)
|
|
{
|
|
char *s, *str = NULL;
|
|
int i, k = 0;
|
|
bool ok = true;
|
|
|
|
if (b)
|
|
s = str = bson_array_as_json(bson, NULL);
|
|
else
|
|
s = str = bson_as_json(bson, NULL);
|
|
|
|
for (i = 0; i < Long && s[i]; i++) {
|
|
switch (s[i]) {
|
|
case ' ':
|
|
if (ok) continue;
|
|
break;
|
|
case '"':
|
|
ok = !ok;
|
|
default:
|
|
break;
|
|
} // endswitch s[i]
|
|
|
|
Mbuf[k++] = s[i];
|
|
} // endfor i
|
|
|
|
bson_free(str);
|
|
|
|
if (i >= Long) {
|
|
snprintf(g->Message, sizeof(g->Message), "Value too long for column %s", Name);
|
|
throw (int)TYPE_AM_MGO;
|
|
} // endif i
|
|
|
|
Mbuf[k] = 0;
|
|
return Mbuf;
|
|
} // end of Mini
|
|
#endif // 0
|
|
|
|
/***********************************************************************/
|
|
/* ReadColumn: */
|
|
/***********************************************************************/
|
|
void JMGCOL::ReadColumn(PGLOBAL g)
|
|
{
|
|
Value->SetValue_psz(Tmgp->Jcp->GetColumnValue(Jpath));
|
|
} // end of ReadColumn
|
|
|
|
/***********************************************************************/
|
|
/* WriteColumn: */
|
|
/***********************************************************************/
|
|
void JMGCOL::WriteColumn(PGLOBAL g)
|
|
{
|
|
// Check whether this node must be written
|
|
if (Value != To_Val)
|
|
Value->SetValue_pval(To_Val, FALSE); // Convert the updated value
|
|
|
|
} // end of WriteColumn
|
|
|
|
#if 0
|
|
/***********************************************************************/
|
|
/* AddValue: Add column value to the document to insert or update. */
|
|
/***********************************************************************/
|
|
bool JMGCOL::AddValue(PGLOBAL g, bson_t *doc, char *key, bool upd)
|
|
{
|
|
bool rc = false;
|
|
|
|
if (Value->IsNull()) {
|
|
if (upd)
|
|
rc = BSON_APPEND_NULL(doc, key);
|
|
else
|
|
return false;
|
|
|
|
} else switch (Buf_Type) {
|
|
case TYPE_STRING:
|
|
rc = BSON_APPEND_UTF8(doc, key, Value->GetCharValue());
|
|
break;
|
|
case TYPE_INT:
|
|
case TYPE_SHORT:
|
|
rc = BSON_APPEND_INT32(doc, key, Value->GetIntValue());
|
|
break;
|
|
case TYPE_TINY:
|
|
rc = BSON_APPEND_BOOL(doc, key, Value->GetIntValue());
|
|
break;
|
|
case TYPE_BIGINT:
|
|
rc = BSON_APPEND_INT64(doc, key, Value->GetBigintValue());
|
|
break;
|
|
case TYPE_DOUBLE:
|
|
rc = BSON_APPEND_DOUBLE(doc, key, Value->GetFloatValue());
|
|
break;
|
|
case TYPE_DECIM:
|
|
{bson_decimal128_t dec;
|
|
|
|
if (bson_decimal128_from_string(Value->GetCharValue(), &dec))
|
|
rc = BSON_APPEND_DECIMAL128(doc, key, &dec);
|
|
|
|
} break;
|
|
case TYPE_DATE:
|
|
rc = BSON_APPEND_DATE_TIME(doc, key, Value->GetBigintValue() * 1000);
|
|
break;
|
|
default:
|
|
snprintf(g->Message, sizeof(g->Message), "Type %d not supported yet", Buf_Type);
|
|
return true;
|
|
} // endswitch Buf_Type
|
|
|
|
if (!rc) {
|
|
strcpy(g->Message, "Adding value failed");
|
|
return true;
|
|
} else
|
|
return false;
|
|
|
|
} // end of AddValue
|
|
#endif // 0
|
|
|
|
/* -------------------------- TDBJGL class --------------------------- */
|
|
|
|
/***********************************************************************/
|
|
/* TDBJGL class constructor. */
|
|
/***********************************************************************/
|
|
TDBJGL::TDBJGL(PMGODEF tdp) : TDBCAT(tdp)
|
|
{
|
|
Topt = tdp->GetTopt();
|
|
Uri = tdp->Uri;
|
|
Db = tdp->GetTabschema();
|
|
} // end of TDBJCL constructor
|
|
|
|
/***********************************************************************/
|
|
/* GetResult: Get the list the MongoDB collection columns. */
|
|
/***********************************************************************/
|
|
PQRYRES TDBJGL::GetResult(PGLOBAL g)
|
|
{
|
|
return MGOColumns(g, Db, Uri, Topt, false);
|
|
} // end of GetResult
|
|
|
|
/* -------------------------- End of mongo --------------------------- */
|