# Copyright 2015 Spanish National Research Council # # 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 copy import json import shlex from six.moves import urllib from ooi import exception _MEDIA_TYPE_MAP = collections.OrderedDict([ ('text/plain', 'text'), ('text/occi', 'header'), ('application/occi+json', 'json'), ]) def _quoted_split(s, separator=',', quotes='"'): """Splits a string considering quotes. e.g. _quoted_split('a,"b,c",d') -> ['a', '"b,c"', 'd'] """ splits = [] partial = [] in_quote = None for c in s: if in_quote: if c == in_quote: in_quote = None else: if c in quotes: in_quote = c if not in_quote and c in separator: if partial: splits.append(''.join(partial)) partial = [] else: partial.append(c) if partial: splits.append(''.join(partial)) return splits def _split_unquote(s, separator="="): """Splits a string considering quotes and removing them in the result. e.g. _split_unquote('a="b=d"') -> ['a', 'b=d'] """ lex = shlex.shlex(s, posix=True) lex.commenters = "" lex.whitespace = separator lex.whitespace_split = True return list(lex) class BaseParser(object): def __init__(self, headers, body): self.headers = headers self.body = body def parse(self): raise NotImplemented class TextParser(BaseParser): def parse_categories(self, headers): kind = action = None mixins = collections.Counter() schemes = collections.defaultdict(list) try: categories = headers["Category"] except KeyError: raise exception.OCCIInvalidSchema("No categories") for ctg in _quoted_split(categories): ll = _quoted_split(ctg, "; ") d = {"term": ll[0]} # assumes 1st element => term's value try: d.update(dict([_split_unquote(i) for i in ll[1:]])) except ValueError: raise exception.OCCIInvalidSchema("Unable to parse category") ctg_class = d.get("class", None) ctg_type = '%(scheme)s%(term)s' % d if ctg_class == "kind": if kind is not None: raise exception.OCCIInvalidSchema("Duplicated Kind") kind = ctg_type elif ctg_class == "action": if action is not None: raise exception.OCCIInvalidSchema("Duplicated action") action = ctg_type elif ctg_class == "mixin": mixins[ctg_type] += 1 schemes[d["scheme"]].append(d["term"]) if action and kind: raise exception.OCCIInvalidSchema("Action and kind together?") return { "category": kind or action, "mixins": mixins, "schemes": schemes, } def parse_attribute_value(self, value): v = value.strip() # quoted: string or bool if v[0] == '"': v = v.strip('"') if v == "true": return True elif v == "false": return False else: return v # unquoted: number or enum-val try: return int(v) except ValueError: try: return float(v) except ValueError: return v def parse_attributes(self, headers): attrs = {} try: header_attrs = headers["X-OCCI-Attribute"] for attr in _quoted_split(header_attrs): try: n, v = attr.split("=", 1) attrs[n.strip()] = self.parse_attribute_value(v) except ValueError: raise exception.OCCIInvalidSchema("Unable to parse") except KeyError: pass return attrs def parse_links(self, headers): links = collections.defaultdict(list) try: header_links = headers["Link"] except KeyError: return links for link in _quoted_split(header_links): ll = _quoted_split(link, "; ") # remove the "<" and ">" if ll[0][1] != "<" and ll[0][-1] != ">": raise exception.OCCIInvalidSchema("Unable to parse link") link_id = ll[0][1:-1] target_location = None target_kind = None attrs = {} try: for attr in ll[1:]: n, v = attr.split("=", 1) n = n.strip().strip('"') v = self.parse_attribute_value(v) if n == "rel": target_kind = v continue elif n == "occi.core.target": target_location = v continue attrs[n] = v except ValueError: raise exception.OCCIInvalidSchema("Unable to parse link") if not (target_kind and target_location): raise exception.OCCIInvalidSchema("Unable to parse link") links[target_kind].append({ "target": target_location, "attributes": attrs, "id": link_id, }) return links def _convert_to_headers(self): if not self.body: raise exception.OCCIInvalidSchema("No schema found") hdrs = collections.defaultdict(list) for l in self.body.splitlines(): hdr, content = l.split(":", 1) hdrs[hdr].append(content) return {hdr: ','.join(hdrs[hdr]) for hdr in hdrs} def _parse(self, headers): obj = self.parse_categories(headers) obj['attributes'] = self.parse_attributes(headers) obj['links'] = self.parse_links(headers) return obj def parse(self): return self._parse(self._convert_to_headers()) class HeaderParser(TextParser): def parse(self): return self._parse(self.headers) class JsonParser(BaseParser): def parse_categories(self, obj): kind = action = None mixins = collections.Counter() schemes = collections.defaultdict(list) if "kind" in obj: sch, term = urllib.parse.urldefrag(obj["kind"]) schemes[sch + "#"].append(term) kind = obj["kind"] for m in obj.get("mixins", []): mixins[m] += 1 sch, term = urllib.parse.urldefrag(m) schemes[sch + "#"].append(term) if "action" in obj: action = obj["action"] sch, term = urllib.parse.urldefrag(obj["action"]) schemes[sch + "#"].append(term) if action and kind: raise exception.OCCIInvalidSchema("Action and kind together?") return { "category": kind or action, "mixins": mixins, "schemes": schemes, } def parse_attributes(self, obj): if "attributes" in obj: return copy.copy(obj["attributes"]) return {} def parse_links(self, obj): links = collections.defaultdict(list) for l in obj.get("links", []): try: d = { "target": l["target"]["location"], "attributes": copy.copy(l.get("attributes", {})), } if "id" in l: d["id"] = l["id"] links[l["target"]["kind"]].append(d) except KeyError: raise exception.OCCIInvalidSchema("Unable to parse link") return links def parse(self): try: obj = json.loads(self.body or "") except ValueError: raise exception.OCCIInvalidSchema("Unable to parse JSON") r = self.parse_categories(obj) r['attributes'] = self.parse_attributes(obj) r['links'] = self.parse_links(obj) return r _PARSERS_MAP = { "text": TextParser, "header": HeaderParser, "json": JsonParser, } def get_media_map(): return _MEDIA_TYPE_MAP def get_default_parsers(): return _PARSERS_MAP def get_supported_content_types(): return _MEDIA_TYPE_MAP.keys()