Completed
Push — master ( b3609d...a307ef )
by Paul
03:04
created

SIP2Response   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Test Coverage

Coverage 96.97%

Importance

Changes 0
Metric Value
wmc 13
eloc 143
dl 0
loc 186
ccs 32
cts 33
cp 0.9697
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 14 4
A checkCRC() 0 4 1
B parseVariableData() 0 45 8
1
<?php
2
3
namespace lordelph\SIP2\Response;
4
5
use lordelph\SIP2\Exception\LogicException;
6
use lordelph\SIP2\Exception\RuntimeException;
7
use lordelph\SIP2\SIP2Message;
8
9
/**
10
 * Class SIP2Response provides a base class for responses and a factory method for constructing them
11
 *
12
 * Derived classes declare the variable data they expect to receive, and provide a parser for the 'fixed'
13
 * fields
14
 *
15
 * @licence    https://opensource.org/licenses/MIT
16
 * @copyright  John Wohlers <[email protected]>
17
 * @copyright  Paul Dixon <[email protected]>
18
 */
19
abstract class SIP2Response extends SIP2Message
20
{
21
    const AA_PATRON_IDENTIFIER = 'AA';
22
    const AB_ITEM_IDENTIFIER = 'AB';
23
    const AE_PERSONAL_NAME = 'AE';
24
    const AF_SCREEN_MESSAGE = 'AF';
25
    const AG_PRINT_LINE = 'AG';
26
    const AH_DUE_DATE = 'AH';
27
    const AJ_TITLE_IDENTIFIER = 'AJ';
28
    const AM_LIBRARY_NAME='AM';
29
    const AN_TERMINAL_LOCATION='AN';
30
    const AO_INSTITUTION_ID = 'AO';
31
    const AP_CURRENT_LOCATION = 'AP';
32
    const AQ_PERMANENT_LOCATION='AQ';
33
    const AS_HOLD_ITEMS = 'AS';
34
    const AT_OVERDUE_ITEMS = 'AT';
35
    const AU_CHARGED_ITEMS = 'AU';
36
    const AV_FINE_ITEMS = 'AV';
37
    const AY_SEQUENCE_NUMBER = 'AY';
38
    const BD_HOME_ADDRESS = 'BD';
39
    const BE_EMAIL_ADDRESS = 'BE';
40
    const BF_HOME_PHONE_NUMBER = 'BF';
41
    const BG_OWNER = 'BG';
42
    const BH_CURRENCY_TYPE = 'BH';
43
    const BK_TRANSACTION_ID= 'BK';
44
    const BL_VALID_PATRON = 'BL';
45
    const BM_RENEWED_ITEMS = 'BM';
46
    const BN_UNRENEWED_ITEMS = 'BN';
47
    const BR_QUEUE_POSITION = 'BR';
48
    const BS_PICKUP_LOCATION = 'BS';
49
    const BT_FEE_TYPE = 'BT';
50
    const BU_RECALL_ITEMS = 'BU';
51
    const BV_FEE_AMOUNT = 'BV';
52
    const BW_EXPIRATION_DATE = 'BW';
53
    const BX_SUPPORTED_MESSAGES='BX';
54
    const BZ_HOLD_ITEMS_LIMIT = 'BZ';
55
    const CA_OVERDUE_ITEMS_LIMIT = 'CA';
56
    const CB_CHARGED_ITEMS_LIMIT = 'CB';
57
    const CC_FEE_LIMIT = 'CC';
58
    const CD_UNAVAILABLE_HOLD_ITEMS = 'CD';
59
    const CF_HOLD_QUEUE_LENGTH = 'CF';
60
    const CH_ITEM_PROPERTIES= 'CH';
61
    const CI_SECURITY_INHIBIT = 'CI';
62
    const CJ_RECALL_DATE = 'CJ';
63
    const CK_MEDIA_TYPE= 'CK';
64
    const CL_SORT_BIN='CL';
65
    const CM_HOLD_PICKUP_DATE = 'CM';
66
    const CQ_VALID_PATRON_PASSWORD = 'CQ';
67
68
    /** @var array maps SIP2 numeric response codes onto response classes */
69
    private static $mapResponseToClass = [
70
        '10' => CheckInResponse::class,
71
        '12' => CheckOutResponse::class,
72
        '16' => HoldResponse::class,
73
        '18' => ItemInformationResponse::class,
74
        '20' => ItemStatusUpdateResponse::class,
75
        '24' => PatronStatusResponse::class,
76
        '26' => PatronEnableResponse::class,
77
        '30' => RenewResponse::class,
78
        '36' => EndSessionResponse::class,
79
        '38' => FeePaidResponse::class,
80
        '64' => PatronInformationResponse::class,
81
        '66' => RenewAllResponse::class,
82
        '94' => LoginResponse::class,
83
        '98' => ACSStatusResponse::class,
84
    ];
85
86
    /** @var array maps SIP2 variable code names to a definition */
87
    private static $mapCodeToVarDef = [
88
        self::AA_PATRON_IDENTIFIER => ['name' => 'PatronIdentifier', 'default'=>''],
89
        self::AB_ITEM_IDENTIFIER => ['name' => 'ItemIdentifier', 'default'=>''],
90
        self::AE_PERSONAL_NAME => ['name' => 'PersonalName', 'default'=>''],
91
        self::AF_SCREEN_MESSAGE => ['name' => 'ScreenMessage', 'type' => 'array', 'default'=>[]],
92
        self::AG_PRINT_LINE => ['name' => 'PrintLine', 'type' => 'array', 'default'=>[]],
93
        self::AH_DUE_DATE => ['name' => 'DueDate', 'default'=>''],
94
        self::AJ_TITLE_IDENTIFIER => ['name' => 'TitleIdentifier', 'default'=>''],
95
        self::AM_LIBRARY_NAME => ['name' => 'LibraryName', 'default'=>''],
96
        self::AN_TERMINAL_LOCATION => ['name' => 'TerminalLocation', 'default'=>''],
97
        self::AO_INSTITUTION_ID => ['name' => 'InstitutionId', 'default'=>''],
98
        self::AP_CURRENT_LOCATION => ['name' => 'CurrentLocation', 'default'=>''],
99
        self::AQ_PERMANENT_LOCATION => ['name' => 'PermanentLocation', 'default'=>''],
100
        self::AS_HOLD_ITEMS => ['name' => 'HoldItems', 'type' => 'array', 'default'=>[]],
101
        self::AT_OVERDUE_ITEMS => ['name' => 'OverdueItems', 'type' => 'array', 'default'=>[]],
102
        self::AU_CHARGED_ITEMS => ['name' => 'ChargedItems', 'type' => 'array', 'default'=>[]],
103
        self::AV_FINE_ITEMS => ['name' => 'FineItems', 'type' => 'array', 'default'=>[]],
104
        self::AY_SEQUENCE_NUMBER => ['name' => 'SequenceNumber', 'default'=>''],
105
        self::BD_HOME_ADDRESS => ['name' => 'HomeAddress', 'default'=>''],
106
        self::BE_EMAIL_ADDRESS => ['name' => 'EmailAddress', 'default'=>''],
107
        self::BF_HOME_PHONE_NUMBER => ['name' => 'HomePhoneNumber', 'default'=>''],
108
        self::BG_OWNER => ['name' => 'Owner', 'default'=>''],
109
        self::BH_CURRENCY_TYPE => ['name' => 'CurrencyType', 'default'=>''],
110
        self::BK_TRANSACTION_ID => ['name' => 'TransactionId', 'default'=>''],
111
        self::BL_VALID_PATRON => ['name' => 'ValidPatron', 'default'=>''],
112
        self::BM_RENEWED_ITEMS => ['name' => 'RenewedItems', 'type' => 'array', 'default'=>[]],
113
        self::BN_UNRENEWED_ITEMS => ['name' => 'UnrenewedItems', 'type' => 'array', 'default'=>[]],
114
        self::BR_QUEUE_POSITION => ['name' => 'QueuePosition', 'default'=>''],
115
        self::BS_PICKUP_LOCATION => ['name' => 'PickupLocation', 'default'=>''],
116
        self::BT_FEE_TYPE => ['name' => 'FeeType', 'default'=>''],
117
        self::BU_RECALL_ITEMS => ['name' => 'RecallItems', 'type' => 'array', 'default'=>[]],
118
        self::BV_FEE_AMOUNT => ['name' => 'FeeAmount', 'default'=>''],
119
        self::BW_EXPIRATION_DATE => ['name' => 'ExpirationDate', 'default'=>''],
120
        self::BX_SUPPORTED_MESSAGES => ['name' => 'SupportedMessages', 'default'=>''],
121
        self::BZ_HOLD_ITEMS_LIMIT => ['name' => 'HoldItemsLimit', 'default'=>''],
122
        self::CA_OVERDUE_ITEMS_LIMIT => ['name' => 'OverdueItemsLimit', 'default'=>''],
123
        self::CB_CHARGED_ITEMS_LIMIT => ['name' => 'ChargedItemsLimit', 'default'=>''],
124
        self::CC_FEE_LIMIT => ['name' => 'FeeLimit', 'default'=>''],
125
        self::CD_UNAVAILABLE_HOLD_ITEMS => ['name' => 'UnavailableHoldItems', 'type' => 'array', 'default'=>[]],
126
        self::CF_HOLD_QUEUE_LENGTH => ['name' => 'HoldQueueLength', 'default'=>''],
127
        self::CH_ITEM_PROPERTIES => ['name' => 'ItemProperties', 'default'=>''],
128
        self::CI_SECURITY_INHIBIT => ['name' => 'SecurityInhibit', 'default'=>''],
129
        self::CJ_RECALL_DATE => ['name' => 'RecallDate', 'default'=>''],
130
        self::CK_MEDIA_TYPE => ['name' => 'MediaType', 'default'=>''],
131
        self::CL_SORT_BIN => ['name' => 'SortBin', 'default'=>''],
132
        self::CM_HOLD_PICKUP_DATE => ['name' => 'HoldPickupDate', 'default'=>''],
133
        self::CQ_VALID_PATRON_PASSWORD => ['name' => 'ValidPatronPassword', 'default'=>'']
134
    ];
135
136
    protected $allowedVariables = [];
137
138 20
    public static function parse($raw): SIP2Response
139
    {
140 20
        if (empty($raw) || !self::checkCRC($raw)) {
141
            throw new LogicException("Empty string or bad CRC not expected here");//@codeCoverageIgnore
142
        }
143
144 20
        $type = substr($raw, 0, 2);
145 20
        if (!isset(self::$mapResponseToClass[$type])) {
146 1
            throw new RuntimeException("Unexpected SIP2 response $type");
147
        }
148
149
        //good to go
150 19
        $className = self::$mapResponseToClass[$type];
151 19
        return new $className($raw);
152
    }
153
154 21
    public static function checkCRC($raw)
155
    {
156 21
        $test = preg_split('/(.{4})$/', trim($raw), 2, PREG_SPLIT_DELIM_CAPTURE);
157 21
        return self::crc($test[0]) == $test[1];
158
    }
159
160 16
    protected function parseVariableData($response, $start)
161
    {
162
        //init allowed variables
163 16
        foreach ($this->allowedVariables as $code) {
164 16
            if (!isset(self::$mapCodeToVarDef[$code])) {
165
                throw new LogicException("Unexpected $code in allowed variables"); //@codeCoverageIgnore
166
            }
167 16
            $name = self::$mapCodeToVarDef[$code]['name'];
168 16
            if (!$this->hasVariable($name)) {
169
                //add a definition for this variable
170 16
                $this->var[$name] = self::$mapCodeToVarDef[$code];
171
            }
172
        }
173
174 16
        $items = explode("|", substr($response, $start, -7));
175
176 16
        foreach ($items as $item) {
177 16
            $value = substr($item, 2);
178
179
            //we ignore anything with no value
180 16
            $clean = trim($value, "\x00..\x1F");
181 16
            if ($clean==='') {
182
                continue;
183
            }
184
185 16
            $field = substr($item, 0, 2);
186
187
            //expected?
188 16
            if (!in_array($field, $this->allowedVariables)) {
189
                //we tolerate unexpected values and treat them as array types
190
                //named after the code if we don't have a definition for it
191 2
                if (!isset(self::$mapCodeToVarDef[$field])) {
192 1
                    self::$mapCodeToVarDef[$field]=[
193 1
                        'name' => $field,
194 1
                        'type' => 'array'
195
                    ];
196 1
                    $name=$field;
197
                } else {
198 2
                    $name = self::$mapCodeToVarDef[$field]['name'];
199
                }
200 2
                $this->var[$name] = self::$mapCodeToVarDef[$field];
201
            }
202
203 16
            $name = self::$mapCodeToVarDef[$field]['name'];
204 16
            $this->addVariable($name, $clean);
205
206
        }
207 16
    }
208
}
209