forked from corestoreio/pkg
-
Notifications
You must be signed in to change notification settings - Fork 0
/
manager.go
466 lines (417 loc) · 15 KB
/
manager.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
// Copyright 2015, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package store
import (
"errors"
"net/http"
"sync"
"github.com/corestoreio/csfw/config"
"github.com/corestoreio/csfw/storage/csdb"
"github.com/corestoreio/csfw/storage/dbr"
"github.com/dgrijalva/jwt-go"
"github.com/juju/errgo"
)
type (
// Manager uses three internal maps to cache the pointers of Website, Group and Store.
Manager struct {
cr config.Reader
// storage get set of websites, groups and stores and also type assertion to StorageMutator for
// ReInit and Persisting
storage Storager
mu sync.RWMutex
// the next six fields are for internal caching
// map key is a hash value which is generated by either an int64 or a string.
// maybe we can get rid of the map by using the existing slices?
websiteMap map[uint64]*Website
groupMap map[uint64]*Group
storeMap map[uint64]*Store
websites WebsiteSlice
groups GroupSlice
stores StoreSlice
// appStore (*cough*) contains the current selected store from init func. Cannot be cleared
// when booting the app. This store is the main store under which the app runs.
// In Magento slang it is called currentStore but current Store relates to a Store set
// by InitByRequest()
appStore *Store
// defaultStore some one must be always default.
defaultStore *Store
// HealthJob allows profiling and error handling. Default is a noop type
// and can be overridden after creating a new Manager. @todo
// HealthJob health.EventReceiver
}
// ManagerOption option func for NewManager()
ManagerOption func(*Manager)
)
var (
ErrUnsupportedScopeGroup = errors.New("Unsupported scope id")
ErrStoreChangeNotAllowed = errors.New("Store change not allowed")
ErrAppStoreNotSet = errors.New("AppStore is not initialized")
ErrAppStoreSet = errors.New("AppStore already initialized")
ErrHashRetrieverNil = errors.New("Hash argument is nil")
)
// NewManager creates a new store manager which handles websites, store groups and stores.
// @todo Default Storager should be a hardcoded Table* struct ...
func NewManager(opts ...ManagerOption) *Manager {
m := &Manager{
cr: config.DefaultManager,
mu: sync.RWMutex{},
websiteMap: make(map[uint64]*Website),
groupMap: make(map[uint64]*Group),
storeMap: make(map[uint64]*Store),
// HealthJob: utils.HealthJobNoop, @todo
}
for _, opt := range opts {
if opt != nil {
opt(m)
}
}
return m
}
// SetStorage sets the underlying storage system to the Manager. Required option.
func SetManagerStorage(s Storager) ManagerOption {
return func(m *Manager) { m.storage = s }
}
// SetManagerConfig sets the configuration Reader. Optional.
// Default reader is config.DefaultManager
func SetManagerConfig(cr config.Reader) ManagerOption {
return func(m *Manager) { m.cr = cr }
}
// Init initializes the appStore from a scope code and a scope type.
// This function is mainly used when booting the app to set the environment configuration
// Also all other calls to any method receiver with nil arguments depends on the appStore.
// @see \Magento\Store\Model\StorageFactory::_reinitStores
func (sm *Manager) Init(scopeCode config.ScopeIDer, scopeType config.ScopeGroup) error {
if sm.appStore != nil {
return ErrAppStoreSet
}
var err error
switch scopeType {
case config.ScopeStoreID:
sm.appStore, err = sm.Store(scopeCode)
case config.ScopeGroupID:
g, errG := sm.Group(scopeCode) // this is the group_id
if errG != nil {
return errgo.Mask(errG)
}
sm.appStore, err = g.DefaultStore()
break
case config.ScopeWebsiteID:
w, errW := sm.Website(scopeCode)
if errW != nil {
return errgo.Mask(errW)
}
sm.appStore, err = w.DefaultStore()
break
default:
return ErrUnsupportedScopeGroup
}
return errgo.Mask(err)
}
// InitByRequest returns a new Store read from a cookie or HTTP request param.
// The internal appStore must be set before hand.
// 1. check cookie store, always a string and the store code
// 2. check for ___store variable, always a string and the store code
// 3. May return nil,nil if nothing is set.
// This function must be used within an HTTP handler.
// The returned new Store must be used in the HTTP context and overrides the appStore.
// @see \Magento\Store\Model\StorageFactory::_reinitStores
func (sm *Manager) InitByRequest(res http.ResponseWriter, req *http.Request, scopeType config.ScopeGroup) (*Store, error) {
if sm.appStore == nil {
// that means you must call Init() before executing this function.
return nil, ErrAppStoreNotSet
}
var reqStore *Store
if keks := GetCodeFromCookie(req); keks != nil {
reqStore, _ = sm.GetRequestStore(keks, scopeType) // ignore errors
}
if reqStoreCode := req.URL.Query().Get(HTTPRequestParamStore); reqStoreCode != "" {
var err error
// @todo reqStoreCode if number ... cast to int64 because then group id if ScopeGroup is group.
if reqStore, err = sm.GetRequestStore(config.ScopeCode(reqStoreCode), scopeType); err != nil {
return nil, errgo.Mask(err)
}
// also delete and re-set a new cookie
if reqStore != nil && reqStore.Data().Code.String == reqStoreCode {
wds, err := reqStore.Website().DefaultStore()
if err != nil {
return nil, errgo.Mask(err)
}
if wds.Data().Code.String == reqStoreCode {
reqStore.DeleteCookie(res) // cookie not needed anymore
} else {
reqStore.SetCookie(res) // make sure we force set the new store
}
}
}
return reqStore, nil // can be nil,nil
}
// InitByToken returns a Store pointer from a JSON web token. If the store code is invalid,
// this function can return nil,nil
func (sm *Manager) InitByToken(t *jwt.Token, scopeType config.ScopeGroup) (*Store, error) {
if sm.appStore == nil {
// that means you must call Init() before executing this function.
return nil, ErrAppStoreNotSet
}
if tStore := GetCodeFromClaim(t); tStore != nil {
return sm.GetRequestStore(tStore, scopeType)
}
return nil, nil
}
// GetRequestStore is in Magento named setCurrentStore and only used by InitByRequest().
// First argument is the store ID or store code, 2nd arg the scope from the init process.
// Also prevents running a store from another website or store group,
// if website or store group was specified explicitly.
// It returns either an error or the new Store. The returning errors can get ignored because if
// a Store Code is invalid the parent calling function must fall back to the appStore.
// This function must be used within an RPC handler.
func (sm *Manager) GetRequestStore(r config.ScopeIDer, scopeType config.ScopeGroup) (*Store, error) {
if sm.appStore == nil {
// that means you must call Init() before executing this function.
return nil, ErrAppStoreNotSet
}
activeStore, err := sm.activeStore(r) // this is the active store from Cookie or Request.
if activeStore == nil || err != nil {
// store is not active so ignore
return nil, errgo.Mask(err)
}
allowStoreChange := false
switch scopeType {
case config.ScopeStoreID:
allowStoreChange = true
break
case config.ScopeGroupID:
allowStoreChange = activeStore.Data().GroupID == sm.appStore.Data().GroupID
break
case config.ScopeWebsiteID:
allowStoreChange = activeStore.Data().WebsiteID == sm.appStore.Data().WebsiteID
break
}
if allowStoreChange {
return activeStore, nil
}
return nil, ErrStoreChangeNotAllowed
}
// IsSingleStoreMode check if Single-Store mode is enabled in configuration and from Store count < 3.
// This flag only shows that admin does not want to show certain UI components at backend (like store switchers etc)
// if Magento has only one store view but it does not check the store view collection.
func (sm *Manager) IsSingleStoreMode() bool {
return sm.HasSingleStore() && sm.cr.GetBool(config.Path(PathSingleStoreModeEnabled), config.ScopeStore(sm.appStore))
}
// HasSingleStore checks if we only have one store view besides the admin store view.
// Mostly used in models to the set store id and in blocks to not display the store switch.
func (sm *Manager) HasSingleStore() bool {
ss, err := sm.Stores()
if err != nil {
return false
}
// that means: index 0 is admin store and always present plus one more store view.
return ss.Len() < 3
}
// Website returns the cached Website pointer from an ID or code including all of its
// groups and all related stores. It panics when the integrity is incorrect.
// If ID and code are available then the non-empty code has precedence.
// If no argument has been supplied then the Website of the internal appStore
// will be returned. If more than one argument has been provided it returns an error.
func (sm *Manager) Website(r ...config.ScopeIDer) (*Website, error) {
notR := notRetriever(r...)
switch {
case notR && sm.appStore == nil:
return nil, ErrAppStoreNotSet
case notR && sm.appStore != nil:
return sm.appStore.Website(), nil
}
key, err := hash(r[0])
if err != nil {
return nil, err
}
sm.mu.Lock()
defer sm.mu.Unlock()
if w, ok := sm.websiteMap[key]; ok && w != nil {
return w, nil
}
w, err := sm.storage.Website(r[0])
sm.websiteMap[key] = w
return sm.websiteMap[key], errgo.Mask(err)
}
// Websites returns a cached slice containing all pointers to Websites with its associated
// groups and stores. It panics when the integrity is incorrect.
func (sm *Manager) Websites() (WebsiteSlice, error) {
if sm.websites != nil {
return sm.websites, nil
}
var err error
sm.websites, err = sm.storage.Websites()
return sm.websites, err
}
// Group returns a cached Group which contains all related stores and its website.
// Only the argument ID is supported.
// If no argument has been supplied then the Group of the internal appStore
// will be returned. If more than one argument has been provided it returns an error.
func (sm *Manager) Group(r ...config.ScopeIDer) (*Group, error) {
notR := notRetriever(r...)
switch {
case notR && sm.appStore == nil:
return nil, ErrAppStoreNotSet
case notR && sm.appStore != nil:
return sm.appStore.Group(), nil
}
key, err := hash(r[0])
if err != nil {
return nil, err
}
sm.mu.Lock()
defer sm.mu.Unlock()
if g, ok := sm.groupMap[key]; ok && g != nil {
return g, nil
}
g, err := sm.storage.Group(r[0])
sm.groupMap[key] = g
return sm.groupMap[key], errgo.Mask(err)
}
// Groups returns a cached slice containing all pointers to Groups with its associated
// stores and websites. It panics when the integrity is incorrect.
func (sm *Manager) Groups() (GroupSlice, error) {
if sm.groups != nil {
return sm.groups, nil
}
var err error
sm.groups, err = sm.storage.Groups()
return sm.groups, err
}
// Store returns the cached Store view containing its group and its website.
// If ID and code are available then the non-empty code has precedence.
// If no argument has been supplied then the appStore
// will be returned. If more than one argument has been provided it returns an error.
func (sm *Manager) Store(r ...config.ScopeIDer) (*Store, error) {
notR := notRetriever(r...)
switch {
case notR && sm.appStore == nil:
return nil, ErrAppStoreNotSet
case notR && sm.appStore != nil:
return sm.appStore, nil
}
key, err := hash(r[0])
if err != nil {
return nil, err
}
sm.mu.Lock()
defer sm.mu.Unlock()
if s, ok := sm.storeMap[key]; ok && s != nil {
return s, nil
}
s, err := sm.storage.Store(r[0])
sm.storeMap[key] = s
return sm.storeMap[key], errgo.Mask(err)
}
// Stores returns a cached Store slice. Can return an error when the website or
// the group cannot be found.
func (sm *Manager) Stores() (StoreSlice, error) {
if sm.stores != nil {
return sm.stores, nil
}
var err error
sm.stores, err = sm.storage.Stores()
return sm.stores, err
}
// DefaultStoreView returns the default store view.
func (sm *Manager) DefaultStoreView() (*Store, error) {
if sm.defaultStore != nil {
return sm.defaultStore, nil
}
var err error
sm.defaultStore, err = sm.storage.DefaultStoreView()
return sm.defaultStore, err
}
// activeStore returns a new non-cached Store with all its Websites and Groups but only if the Store
// is marked as active. Argument can be an ID or a Code. Returns nil if Store not found or inactive.
// No need here to return an error.
func (sm *Manager) activeStore(r config.ScopeIDer) (*Store, error) {
s, err := sm.storage.Store(r)
if err != nil {
return nil, err
}
if s.Data().IsActive {
return s, nil
}
return nil, ErrStoreNotActive
}
// ReInit reloads the website, store group and store view data from the database.
// After reloading internal cache will be cleared if there are no errors.
func (sm *Manager) ReInit(dbrSess dbr.SessionRunner, cbs ...csdb.DbrSelectCb) error {
err := sm.storage.ReInit(dbrSess, cbs...)
if err == nil {
sm.ClearCache()
}
return err
}
// ClearCache resets the internal caches which stores the pointers to a Website, Group or Store and
// all related slices. Please use with caution. ReInit() also uses this method.
// Providing argument true clears also the internal appStore cache.
func (sm *Manager) ClearCache(clearAll ...bool) {
sm.mu.Lock()
defer sm.mu.Unlock()
if len(sm.websiteMap) > 0 {
for k := range sm.websiteMap {
delete(sm.websiteMap, k)
}
}
if len(sm.groupMap) > 0 {
for k := range sm.groupMap {
delete(sm.groupMap, k)
}
}
if len(sm.storeMap) > 0 {
for k := range sm.storeMap {
delete(sm.storeMap, k)
}
}
sm.websites = nil
sm.groups = nil
sm.stores = nil
sm.defaultStore = nil
// do not clear currentStore as this one depends on the init funcs
if 1 == len(clearAll) && clearAll[0] {
sm.appStore = nil
}
}
// IsCacheEmpty returns true if the internal cache is empty.
func (sm *Manager) IsCacheEmpty() bool {
return len(sm.websiteMap) == 0 && len(sm.groupMap) == 0 && len(sm.storeMap) == 0 &&
sm.websites == nil && sm.groups == nil && sm.stores == nil && sm.defaultStore == nil
}
// notRetriever checks if variadic ScopeIDer is nil or has more than two entries
// or the first index is nil.
func notRetriever(r ...config.ScopeIDer) bool {
lr := len(r)
return r == nil || (lr == 1 && r[0] == nil) || lr > 1
}
// hash generates the key for the map from either an id int64 or a code string.
// If both interfaces are nil it returns 0 which is default for website, group or store.
// fnv64a used to calculate the uint64 value of a string, especially website code and store code.
func hash(r config.ScopeIDer) (uint64, error) {
uz := uint64(0)
if r == nil {
return uz, ErrHashRetrieverNil
}
if c, ok := r.(config.ScopeCoder); ok && c.ScopeCode() != "" {
data := []byte(c.ScopeCode())
var hash uint64 = 14695981039346656037
for _, c := range data {
hash ^= uint64(c)
hash *= 1099511628211
}
return hash, nil
}
return uint64(r.ScopeID()), nil
}