1<?php
2/*
3    Copyright (C) 2016 Volker Krause <vkrause@kde.org>
4
5    Permission is hereby granted, free of charge, to any person obtaining
6    a copy of this software and associated documentation files (the
7    "Software"), to deal in the Software without restriction, including
8    without limitation the rights to use, copy, modify, merge, publish,
9    distribute, sublicense, and/or sell copies of the Software, and to
10    permit persons to whom the Software is furnished to do so, subject to
11    the following conditions:
12
13    The above copyright notice and this permission notice shall be included
14    in all copies or substantial portions of the Software.
15
16    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23*/
24
25require_once('restexception.php');
26require_once('utils.php');
27
28/** Represents a product schema entry element. */
29class SchemaEntryElement
30{
31    public $name;
32    public $type;
33
34    private $schemaEntry = null;
35
36    const STRING_TYPE = 'string';
37    const INT_TYPE = 'int';
38    const NUMBER_TYPE = 'number';
39    const BOOL_TYPE = 'bool';
40
41    public function __construct(SchemaEntry $entry)
42    {
43        $this->schemaEntry = &$entry;
44    }
45
46    /** Checks if this schema entry element is valid. */
47    public function isValid()
48    {
49        if ($this->type != self::STRING_TYPE && $this->type != self::INT_TYPE && $this->type != self::NUMBER_TYPE && $this->type != self::BOOL_TYPE)
50            return false;
51        if (!Utils::isValidIdentifier($this->name))
52            return false;
53        return true;
54    }
55
56    /** Column name in the data table. */
57    public function dataColumnName()
58    {
59        $colName = 'col_data_' . Utils::normalizeString($this->schemaEntry->name) . '_' . Utils::normalizeString($this->name);
60        return strtolower($colName);
61    }
62
63    /** Insert this element into storage. */
64    public function insert(Datastore $db, $entryId)
65    {
66        $stmt = $db->prepare('INSERT INTO
67            tbl_schema_element (col_schema_id, col_name, col_type)
68            VALUES (:schemaId, :name, :type)
69        ');
70        $stmt->bindValue(':schemaId', $entryId, PDO::PARAM_INT);
71        $stmt->bindValue(':name', $this->name, PDO::PARAM_STR);
72        $stmt->bindValue(':type', $this->type, PDO::PARAM_STR);
73        $db->execute($stmt);
74
75        if ($this->schemaEntry->isScalar())
76            $this->createScalarDataTableColumn($db);
77        else
78            $this->createNonScalarDataTableColumn($db);
79    }
80
81    /** Delete this element from storage. */
82    public function delete(Datastore $db, $entryId)
83    {
84        $stmt = $db->prepare('
85            DELETE FROM tbl_schema_element
86            WHERE col_schema_id = :schemaId AND col_name = :name
87        ');
88        $stmt->bindValue(':schemaId', $entryId, PDO::PARAM_INT);
89        $stmt->bindValue(':name', $this->name, PDO::PARAM_STR);
90        $db->execute($stmt);
91    }
92
93    /** Drop data table column for this element. */
94    public function dropDataColumn(Datastore $db)
95    {
96        if ($db->driver() == 'sqlite') {
97            error_log('Sqlite does not support dropping columns.');
98            return;
99        }
100
101        try {
102            if ($this->schemaEntry->isScalar()) {
103                $stmt = $db->prepare('ALTER TABLE ' . $this->schemaEntry->product()->dataTableName()
104                    . ' DROP COLUMN ' . $this->dataColumnName());
105                $db->execute($stmt);
106            } else {
107                $stmt = $db->prepare('ALTER TABLE ' . $this->schemaEntry->dataTableName()
108                    . ' DROP COLUMN ' . $this->dataColumnName());
109                $db->execute($stmt);
110            }
111        } catch (RESTException $e) {
112            // don't fail hard on column removal, this can leave the db and our schema description in an inconsistent state
113            // or block product deletion entirely
114            error_log($e->getMessage());
115            // restart transaction so this doesn't block subsequent queries
116            $db->rollBack();
117            $db->beginTransaction();
118        }
119    }
120
121    /** Convert a JSON array into an array of SchemaEntryElement instances. */
122    static public function fromJson($jsonArray, SchemaEntry &$entry)
123    {
124        $elems = array();
125        foreach ($jsonArray as $jsonObj) {
126            if (!property_exists($jsonObj, 'name') || !property_exists($jsonObj, 'type'))
127                throw new RESTException('Incomplete schema entry element.', 400);
128            $e = new SchemaEntryElement($entry);
129            $e->name = $jsonObj->name;
130            $e->type = $jsonObj->type;
131            if (!$e->isValid())
132                throw new RESTException('Invalid schema entry element.', 400);
133            array_push($elems, $e);
134        }
135        return $elems;
136    }
137
138
139    /** SQL type for this element. */
140    private function sqlType(Datastore $db)
141    {
142        switch ($this->type) {
143            case self::STRING_TYPE: return Utils::sqlStringType($db->driver());
144            case self::INT_TYPE: return 'INTEGER';
145            case self::NUMBER_TYPE: return 'REAL';
146            case self::BOOL_TYPE: return 'BOOLEAN';
147        }
148        throw new RESTException('Unsupported schema entry element type.', 400);
149    }
150
151    /** Creates a data table entry for scalar elements. */
152    private function createScalarDataTableColumn(Datastore $db)
153    {
154        $stmt = $db->prepare('ALTER TABLE ' . $this->schemaEntry->product()->dataTableName()
155            . ' ADD COLUMN ' . $this->dataColumnName() . ' ' . $this->sqlType($db));
156        $db->execute($stmt);
157    }
158
159    /** Creates a data table entry for non-scalar elements. */
160    private function createNonScalarDataTableColumn(Datastore $db)
161    {
162        $stmt = $db->prepare('ALTER TABLE ' . $this->schemaEntry->dataTableName()
163            . ' ADD COLUMN ' . $this->dataColumnName() . ' ' . $this->sqlType($db));
164        $db->execute($stmt);
165    }
166}
167
168?>
169