# Copyright (c) 2013 Mirantis, Inc. # # 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. import collections import functools as func import re import jsonschema from oslo_log import log as logging import six from murano.common.i18n import _ LOG = logging.getLogger(__name__) class TraverseHelper(object): value_type = (six.string_types, int, float, bool) @staticmethod def get(path, source): """Provides the ability to traverse a data source Provides the ability to traverse a data source made up of any combination of lists and dicts. Has simple rules for selecting item of the list: * each item should have id property * to select item from the list, specify id value Examples: source = {'obj': {'attr': True}} value = TraverseHelper.get('/obj/attr', source) source = {'obj': [ {'id': '1', 'value': 1}, {'id': '2s', 'value': 2}, ]} value = TraverseHelper.get('/obj/2s/value', source) :param path: string with path to desired value :param source: python object (list or dict) :return: object :raise: ValueError if object is malformed """ queue = collections.deque(filter(lambda x: x, path.split('/'))) while len(queue): path = queue.popleft() if isinstance(source, list): idx_source = source iterator = ( i for i in source if i.get('?', {}).get('id') == path ) source = next(iterator, None) if source is None and path.isdigit(): source = idx_source[int(path)] elif isinstance(source, dict): source = source[path] elif isinstance(source, TraverseHelper.value_type): break else: raise ValueError(_('Source object or path is malformed')) return source @staticmethod def update(path, value, source): """Updates value selected with specified path. Warning: Root object could not be updated :param path: string with path to desired value :param value: value :param source: python object (list or dict) """ parent_path = '/'.join(path.split('/')[:-1]) node = TraverseHelper.get(parent_path, source) key = path[1:].split('/')[-1] if is_number(key): node[int(key)] = value else: node[key] = value @staticmethod def insert(path, value, source): """Inserts new item to selected list. :param path: string with path to desired value :param value: value :param source: List """ node = TraverseHelper.get(path, source) node.append(value) @staticmethod def extend(path, value, source): """Extend list by appending elements from the iterable. :param path: string with path to desired value :param value: value :param source: List """ node = TraverseHelper.get(path, source) node.extend(value) @staticmethod def remove(path, source): """Removes selected item from source. :param path: string with path to desired value :param source: python object (list or dict) """ parent_path = '/'.join(path.split('/')[:-1]) node = TraverseHelper.get(parent_path, source) key = path[1:].split('/')[-1] if isinstance(node, list): iterator = (i for i in node if i.get('?', {}).get('id') == key) item = next(iterator, None) if item is None and key.isdigit(): del node[int(key)] else: node.remove(item) elif isinstance(node, dict): del node[key] else: raise ValueError(_('Source object or path is malformed')) def is_number(var): try: int(var) return True except Exception: return False def is_different(obj1, obj2): """Stripped-down version of deep.diff comparator Compares arbitrary nested objects, handles circular links, but doesn't point to the first difference as deep.diff does. """ class Difference(Exception): pass def is_in(o, st): for _o in st: if o is _o: return True return False def rec(o1, o2, stack1=(), stack2=()): if is_in(o1, stack1) and is_in(o2, stack2): # circular reference detected - break the loop return elif is_in(o1, stack1): raise Difference() else: stack1 += (o1,) stack2 += (o2,) if o1 is o2: return elif (isinstance(o1, six.string_types) and isinstance(o2, six.string_types)) and o1 == o2: return elif type(o1) != type(o2): raise Difference() elif isinstance(o1, dict): # check for keys inequality rec(o1.keys(), o2.keys(), stack1, stack2) for key in o1.keys(): rec(o1[key], o2[key], stack1, stack2) elif isinstance(o1, (list, tuple, set)): if len(o1) != len(o2): raise Difference() else: for _o1, _o2 in zip(o1, o2): rec(_o1, _o2, stack1, stack2) elif hasattr(o1, '__dict__'): return rec(o1.__dict__, o2.__dict__, stack1, stack2) elif o1 != o2: raise Difference() try: rec(obj1, obj2) except Difference: return True else: return False def build_entity_map(value): def build_entity_map_recursive(value, id_map): if isinstance(value, dict): if '?' in value and 'id' in value['?']: id_map[value['?']['id']] = value for v in six.itervalues(value): build_entity_map_recursive(v, id_map) if isinstance(value, list): for item in value: build_entity_map_recursive(item, id_map) id_map = {} build_entity_map_recursive(value, id_map) return id_map def handle(f): """Handles exception in wrapped function and writes to LOG.""" @func.wraps(f) def f_handle(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: LOG.exception(e) return f_handle def validate_body(schema): def deco_validate_body(f): @func.wraps(f) def f_validate_body(*args, **kwargs): if 'body' in kwargs: jsonschema.validate(kwargs['body'], schema) return f(*args, **kwargs) return f_validate_body return deco_validate_body def validate_quotes(value): """Validate filter values Validation opening/closing quotes in the expression. """ open_quotes = True count_backslash_in_row = 0 for i in range(len(value)): if value[i] == '"': if count_backslash_in_row % 2: continue if open_quotes: if i and value[i - 1] != ',': msg = _("Invalid filter value %s. There is no comma " "before opening quotation mark.") % value raise ValueError(msg) else: if i + 1 != len(value) and value[i + 1] != ",": msg = _("Invalid filter value %s. There is no comma " "after opening quotation mark.") % value raise ValueError(msg) open_quotes = not open_quotes elif value[i] == '\\': count_backslash_in_row += 1 else: count_backslash_in_row = 0 if not open_quotes: msg = _("Invalid filter value %s. The quote is not closed.") % value raise ValueError(msg) return True def split_for_quotes(value): """Split filter values Split values by commas and quotes for 'in' operator, according api-wg. """ validate_quotes(value) tmp = re.compile(r''' "( # if found a double-quote [^\"\\]* # take characters either non-quotes or backslashes (?:\\. # take backslashes and character after it [^\"\\]*)* # take characters either non-quotes or backslashes ) # before double-quote ",? # a double-quote with comma maybe | ([^,]+),? # if not found double-quote take any non-comma # characters with comma maybe | , # if we have only comma take empty string ''', re.VERBOSE) val_split = [val[0] or val[1] for val in re.findall(tmp, value)] replaced_inner_quotes = [s.replace(r'\"', '"') for s in val_split] return replaced_inner_quotes