// -*- Mode: Go; indent-tabs-mode: t -*- // // Copyright (C) 2020-2023 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package cache import ( "fmt" "regexp" "sync" "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" "github.com/edgexfoundry/go-mod-core-contracts/v4/models" ) var ( pc *profileCache ) type ProfileCache interface { ForName(name string) (models.DeviceProfile, bool) All() []models.DeviceProfile Add(profile models.DeviceProfile) errors.EdgeX Update(profile models.DeviceProfile) errors.EdgeX RemoveByName(name string) errors.EdgeX DeviceResource(profileName string, resourceName string) (models.DeviceResource, bool) DeviceResourcesByRegex(profileName string, regex *regexp.Regexp) ([]models.DeviceResource, bool) DeviceCommand(profileName string, commandName string) (models.DeviceCommand, bool) ResourceOperation(profileName string, deviceResource string) (models.ResourceOperation, errors.EdgeX) CheckAndAdd(profile models.DeviceProfile) errors.EdgeX } type profileCache struct { deviceProfileMap map[string]*models.DeviceProfile // key is DeviceProfile name deviceResourceMap map[string]map[string]models.DeviceResource deviceCommandMap map[string]map[string]models.DeviceCommand mutex sync.RWMutex } func newProfileCache(profiles []models.DeviceProfile) ProfileCache { defaultSize := len(profiles) dpMap := make(map[string]*models.DeviceProfile, defaultSize) drMap := make(map[string]map[string]models.DeviceResource, defaultSize) dcMap := make(map[string]map[string]models.DeviceCommand, defaultSize) for i, dp := range profiles { dpMap[dp.Name] = &profiles[i] drMap[dp.Name] = deviceResourceSliceToMap(dp.DeviceResources) dcMap[dp.Name] = deviceCommandSliceToMap(dp.DeviceCommands) } pc = &profileCache{ deviceProfileMap: dpMap, deviceResourceMap: drMap, deviceCommandMap: dcMap, } return pc } // ForName returns a profile with the given profile name. func (p *profileCache) ForName(name string) (models.DeviceProfile, bool) { p.mutex.RLock() defer p.mutex.RUnlock() profile, ok := p.deviceProfileMap[name] if !ok { return models.DeviceProfile{}, false } return *profile, ok } // All returns the current list of profiles in the cache. func (p *profileCache) All() []models.DeviceProfile { p.mutex.RLock() defer p.mutex.RUnlock() i := 0 ps := make([]models.DeviceProfile, len(p.deviceProfileMap)) for _, profile := range p.deviceProfileMap { ps[i] = *profile i += 1 } return ps } // Add adds a new profile to the cache. This method is used to populate the // profile cache with pre-existing or recently-added profiles from Core Metadata. func (p *profileCache) Add(profile models.DeviceProfile) errors.EdgeX { p.mutex.Lock() defer p.mutex.Unlock() return p.add(profile) } func (p *profileCache) CheckAndAdd(profile models.DeviceProfile) errors.EdgeX { p.mutex.Lock() defer p.mutex.Unlock() if _, ok := p.deviceProfileMap[profile.Name]; ok { return nil } else { return p.add(profile) } } func (p *profileCache) add(profile models.DeviceProfile) errors.EdgeX { if _, ok := p.deviceProfileMap[profile.Name]; ok { errMsg := fmt.Sprintf("Profile %s has already existed in cache", profile.Name) return errors.NewCommonEdgeX(errors.KindDuplicateName, errMsg, nil) } p.deviceProfileMap[profile.Name] = &profile p.deviceResourceMap[profile.Name] = deviceResourceSliceToMap(profile.DeviceResources) p.deviceCommandMap[profile.Name] = deviceCommandSliceToMap(profile.DeviceCommands) return nil } func deviceResourceSliceToMap(deviceResources []models.DeviceResource) map[string]models.DeviceResource { result := make(map[string]models.DeviceResource, len(deviceResources)) for _, dr := range deviceResources { result[dr.Name] = dr } return result } func deviceCommandSliceToMap(deviceCommands []models.DeviceCommand) map[string]models.DeviceCommand { result := make(map[string]models.DeviceCommand, len(deviceCommands)) for _, dc := range deviceCommands { result[dc.Name] = dc } return result } // Update updates the profile in the cache func (p *profileCache) Update(profile models.DeviceProfile) errors.EdgeX { p.mutex.Lock() defer p.mutex.Unlock() if err := p.removeByName(profile.Name); err != nil { return err } return p.add(profile) } // RemoveByName removes the specified profile by name from the cache. func (p *profileCache) RemoveByName(name string) errors.EdgeX { p.mutex.Lock() defer p.mutex.Unlock() return p.removeByName(name) } func (p *profileCache) removeByName(name string) errors.EdgeX { _, ok := p.deviceProfileMap[name] if !ok { errMsg := fmt.Sprintf("failed to find Profile %s in cache", name) return errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil) } delete(p.deviceProfileMap, name) delete(p.deviceResourceMap, name) delete(p.deviceCommandMap, name) return nil } // DeviceResource returns the DeviceResource with given profileName and resourceName func (p *profileCache) DeviceResource(profileName string, resourceName string) (models.DeviceResource, bool) { p.mutex.RLock() defer p.mutex.RUnlock() drs, ok := p.deviceResourceMap[profileName] if !ok { return models.DeviceResource{}, false } dr, ok := drs[resourceName] return dr, ok } // DeviceResourcesByRegex returns matched DeviceResource list with given profileName and regex pattern func (p *profileCache) DeviceResourcesByRegex(profileName string, regex *regexp.Regexp) ([]models.DeviceResource, bool) { p.mutex.RLock() defer p.mutex.RUnlock() drs, ok := p.deviceResourceMap[profileName] if !ok { return nil, false } var res []models.DeviceResource for _, dr := range drs { if dr.Name == regex.String() { res = append(res, dr) continue } matchString := regex.FindString(dr.Name) if matchString == dr.Name { res = append(res, dr) } } return res, true } // DeviceCommand returns the DeviceCommand with given profileName and commandName func (p *profileCache) DeviceCommand(profileName string, commandName string) (models.DeviceCommand, bool) { p.mutex.RLock() defer p.mutex.RUnlock() dcs, ok := p.deviceCommandMap[profileName] if !ok { return models.DeviceCommand{}, false } dc, ok := dcs[commandName] return dc, ok } // ResourceOperation returns the first matched ResourceOperation with given resourceName func (p *profileCache) ResourceOperation(profileName string, resourceName string) (models.ResourceOperation, errors.EdgeX) { p.mutex.RLock() defer p.mutex.RUnlock() if err := p.verifyProfileExists(profileName); err != nil { return models.ResourceOperation{}, err } deviceCommandMap := p.deviceCommandMap[profileName] for _, dc := range deviceCommandMap { for _, ro := range dc.ResourceOperations { if ro.DeviceResource == resourceName { return ro, nil } } } errMsg := fmt.Sprintf("failed to find ResourceOpertaion with DeviceResource %s in Profile %s", resourceName, profileName) return models.ResourceOperation{}, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil) } func (p *profileCache) verifyProfileExists(profileName string) errors.EdgeX { if _, ok := p.deviceProfileMap[profileName]; !ok { errMsg := fmt.Sprintf("failed to find Profile %s in cache", profileName) return errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil) } return nil } func Profiles() ProfileCache { return pc }