# Source code for pywps.inout.basic

    ##################################################################
    # Copyright 2018 Open Source Geospatial Foundation and others    #
    # licensed under MIT, Please consult LICENSE.txt for details     #
    ##################################################################
    import base64
    import json
    import logging
    import os
    import shutil
    import tempfile
    import weakref
    from collections import namedtuple
    from copy import deepcopy
    from io import BytesIO, StringIO, open
    from pathlib import PurePath
    from urllib.parse import urlparse

    import humanize
    import requests

    import pywps.configuration as config
    from pywps import OGCUNIT
    from pywps.exceptions import (
        FileSizeExceeded,
        FileURLNotSupported,
        InvalidParameterValue,
        NoApplicableCode,
    )
    from pywps.inout.formats import Supported_Formats
    from pywps.inout.literaltypes import (
        LITERAL_DATA_TYPES,
        convert,
        is_anyvalue,
        is_values_reference,
        make_allowedvalues,
    )
    from pywps.inout.types import Translations
    from pywps.translations import lower_case_dict
    from pywps.validator import get_validator
    from pywps.validator.base import emptyvalidator
    from pywps.validator.literalvalidator import (
        validate_allowed_values,
        validate_anyvalue,
        validate_value,
        validate_values_reference,
    )
    from pywps.validator.mode import MODE

    _SOURCE_TYPE = namedtuple('SOURCE_TYPE', 'MEMORY, FILE, STREAM, DATA, URL')
    SOURCE_TYPE = _SOURCE_TYPE(0, 1, 2, 3, 4)

    LOGGER = logging.getLogger("PYWPS")

    def _is_textfile(filename):
        try:
            # use python-magic if available
            import magic
            is_text = 'text/' in magic.from_file(filename, mime=True)
        except ImportError:
            # read the first part of the file to check for a binary indicator.
            # This method won't detect all binary files.
            blocksize = 512
            fh = open(filename, 'rb')
            is_text = b'\x00' not in fh.read(blocksize)
            fh.close()
        return is_text

    class UOM(object):
        """
        :param uom: unit of measure
        """

        def __init__(self, uom='', reference=None):
            self.uom = uom
            self.reference = reference

            if self.reference is None:
                self.reference = OGCUNIT.get(self.uom, '')

        @property
        def json(self):
            return {"reference": self.reference,
                    "uom": self.uom}

        def __eq__(self, other):
            return self.uom == other.uom

    class NoneIOHandler(object):
        """Base class for implementation of IOHandler internal"""

        prop = None

        def __init__(self, ref):
            self._ref = weakref.ref(ref)

        @property
        def file(self):
            """Return filename."""
            return None

        @property
        def data(self):
            """Read file and return content."""
            return None

        @property
        def base64(self):
            """Return base64 encoding of data."""
            return None

        @property
        def stream(self):
            """Return stream object."""
            return None

        @property
        def mem(self):
            """Return memory object."""
            return None

        @property
        def url(self):
            """Return url to file."""
            return None

        @property
        def size(self):
            """Length of the linked content in octets."""
            return None

        @property
        def post_data(self):
            raise NotImplementedError

        # Will raise an error if used on invalid object
        @post_data.setter
        def post_data(self, value):
            raise NotImplementedError

    class IOHandler(object):
        """Base IO handling class that handle multple IO types

        This class is created with NoneIOHandler that have no data
        inside. To initialise data you can set the `file`, `url`, `data` or
        `stream` attribute. If reset one of this attribute old data are lost and
        replaced by the new one.

        :param workdir: working directory, to save temporal file objects in.
        :param mode: ``MODE`` validation mode.

        `file` : str
          Filename on the local disk.
        `url` : str
          Link to an online resource.
        `stream` : FileIO
          A readable object.
        `data` : object
          A native python object (integer, string, float, etc)
        `base64` : str
          A base 64 encoding of the data.

        >>> # setting up
        >>> import os
        >>> from io import RawIOBase
        >>> from io import FileIO
        >>>
        >>> ioh_file = IOHandler(workdir=tmp)
        >>> assert isinstance(ioh_file, IOHandler)
        >>>
        >>> # Create test file input
        >>> fileobj = open(os.path.join(tmp, 'myfile.txt'), 'w')
        >>> fileobj.write('ASDF ASFADSF ASF ASF ASDF ASFASF')
        >>> fileobj.close()
        >>>
        >>> # testing file object on input
        >>> ioh_file.file = fileobj.name
        >>> assert ioh_file.file == fileobj.name
        >>> assert isinstance(ioh_file.stream, RawIOBase)
        >>> # skipped assert isinstance(ioh_file.memory_object, POSH)
        >>>
        >>> # testing stream object on input
        >>> ioh_stream = IOHandler(workdir=tmp)
        >>> assert ioh_stream.workdir == tmp
        >>> ioh_stream.stream = FileIO(fileobj.name,'r')
        >>> assert open(ioh_stream.file).read() == ioh_file.stream.read()
        >>> assert isinstance(ioh_stream.stream, RawIOBase)
        """

        def __init__(self, workdir=None, mode=MODE.NONE):

            self._iohandler = NoneIOHandler(self)

            # Internal defaults for class and subclass properties.
            self._workdir = None

            # Set public defaults
            self.workdir = workdir
            self.valid_mode = mode

            # TODO: Clarify intent
            self.as_reference = False
            self.inpt = {}
            self.uuid = None  # request identifier
            self.data_set = False

        def _check_valid(self):
            """Validate this input using given validator
            """

            validate = self.validator
            if validate is not None:
                _valid = validate(self, self.valid_mode)
                if not _valid:
                    self.data_set = False
                    raise InvalidParameterValue('Input data not valid using '
                                                'mode {}'.format(self.valid_mode))
            self.data_set = True

        @property
        def workdir(self):
            return self._workdir

        @workdir.setter
        def workdir(self, path):
            """Set working temporary directory for files to be stored in."""

            if path is not None:
                if not os.path.exists(path):
                    os.makedirs(path)

            self._workdir = path

        @property
        def validator(self):
            """Return the function suitable for validation
            This method should be overridden by class children

            :return: validating function
            """

            return emptyvalidator

        @property
        def source_type(self):
            """Return the source type."""
            # For backward compatibility only. source_type checks could be replaced by `isinstance`.
            return getattr(SOURCE_TYPE, self.prop.upper())

        def _set_default_value(self, value=None, value_type=None):
            """Set default value based on input data type."""
            value = value or getattr(self, '_default')
            value_type = value_type or getattr(self, '_default_type')

            if value:
                # only set default when a value is optional
                if self.min_occurs == 0:
                    if value_type == SOURCE_TYPE.DATA:
                        self.data = value
                    elif value_type == SOURCE_TYPE.MEMORY:
                        raise NotImplementedError
                    elif value_type == SOURCE_TYPE.FILE:
                        self.file = value
                    elif value_type == SOURCE_TYPE.STREAM:
                        self.stream = value
                    elif value_type == SOURCE_TYPE.URL:
                        self.url = value
                else:
                    # when a value is requried the default value will be ignored
                    LOGGER.warning(
                        "The given default value will not be used"
                        " because is is required to provide a value.")

        def _build_file_name(self, href=''):
            """Return a file name for the local system."""
            url_path = urlparse(href).path or ''
            file_name = os.path.basename(url_path).strip() or 'input'
            (prefix, suffix) = os.path.splitext(file_name)
            suffix = suffix or self.extension
            if prefix and suffix:
                file_name = prefix + suffix
            input_file_name = os.path.join(self.workdir, file_name)

            # build tempfile in case of duplicates
            if os.path.exists(input_file_name):
                input_file_name = tempfile.mkstemp(
                    suffix=suffix, prefix=prefix + '_',
                    dir=self.workdir)[1]

            return input_file_name

        @property
        def extension(self):
            """Return the file extension for the data format, if set."""
            if getattr(self, 'data_format', None):
                return self.data_format.extension
            else:
                return ''

        def clone(self):
            """Create copy of yourself
            """
            return deepcopy(self)

        @property
        def base64(self):
            """Return raw data
            WARNING: may be bytes or str"""
            return self._iohandler.base64

        @property
        def size(self):
            """Return object size in bytes.
            """
            return self._iohandler.size

        @property
        def file(self):
            """Return a file name"""
            return self._iohandler.file

        @file.setter
        def file(self, value):
            self._iohandler = FileHandler(value, self)
            self._check_valid()

        @property
        def data(self):
            """Return raw data
            WARNING: may be bytes or str"""
            return self._iohandler.data

        def data_as_json(self):
            # applies json.loads if needed
            data = self._iohandler.data
            if data and not isinstance(self._iohandler, DataHandler) and self.extension in ['.geojson', 'json']:
                data = json.loads(data)
                self.data = data  # switch to a DataHandler
            return data

        @data.setter
        def data(self, value):
            self._iohandler = DataHandler(value, self)
            self._check_valid()

        @property
        def stream(self):
            """Return stream of data
            WARNING: may be FileIO or StringIO"""
            return self._iohandler.stream

        @stream.setter
        def stream(self, value):
            self._iohandler = StreamHandler(value, self)
            self._check_valid()

        @property
        def url(self):
            """Return the url of data"""
            return self._iohandler.url

        @url.setter
        def url(self, value):
            self._iohandler = UrlHandler(value, self)
            self._check_valid()

        # FIXME: post_data is only related to url, this should be initialize with url setter
        @property
        def post_data(self):
            return self._iohandler.post_data

        # Will raise an arror if used on invalid object
        @post_data.setter
        def post_data(self, value):
            self._iohandler.post_data = value

        @property
        def prop(self):
            return self._iohandler.prop

    class FileHandler(NoneIOHandler):
        prop = 'file'

        def __init__(self, value, ref):
            self._ref = weakref.ref(ref)
            self._data = None
            self._stream = None
            self._file = os.path.abspath(value)

        @property
        def file(self):
            """Return filename."""
            return self._file

        @property
        def data(self):
            """Read file and return content."""
            if self._data is None:
                openmode = self._openmode(self._ref())
                kwargs = {} if 'b' in openmode else {'encoding': 'utf8'}
                with open(self.file, mode=openmode, **kwargs) as fh:
                    self._data = fh.read()
            return self._data

        @property
        def base64(self):
            """Return base64 encoding of data."""
            data = self.data.encode() if not isinstance(self.data, bytes) else self.data
            return base64.b64encode(data)

        @property
        def stream(self):
            """Return stream object."""
            from io import FileIO
            if self._stream and not self._stream.closed:
                self._stream.close()

            self._stream = FileIO(self.file, mode='r', closefd=True)
            return self._stream

        @property
        def url(self):
            """Return url to file."""
            result = PurePath(self.file).as_uri()
            return result

        @property
        def size(self):
            """Length of the linked content in octets."""
            return os.stat(self.file).st_size

        def _openmode(self, base, data=None):
            openmode = 'r'
            # in Python 3 we need to open binary files in binary mode.
            checked = False
            if hasattr(base, 'data_format'):
                if base.data_format.encoding == 'base64':
                    # binary, when the data is to be encoded to base64
                    openmode += 'b'
                    checked = True
                elif 'text/' in base.data_format.mime_type:
                    # not binary, when mime_type is 'text/'
                    checked = True
            # when we can't guess it from the mime_type, we need to check the file.
            # mimetypes like application/xml and application/json are text files too.
            if not checked and not _is_textfile(self.file):
                openmode += 'b'
            return openmode

    class DataHandler(FileHandler):
        prop = 'data'

        def __init__(self, value, ref):
            self._ref = weakref.ref(ref)
            self._file = None
            self._stream = None
            self._data = value

        def _openmode(self, data=None):
            openmode = 'w'
            if isinstance(data, bytes):
                # on Python 3 open the file in binary mode if the source is
                # bytes, which happens when the data was base64-decoded
                openmode += 'b'
            return openmode

        @property
        def data(self):
            """Return data."""
            return self._data

        @property
        def file(self):
            """Return file name storing the data.

            Requesting the file attributes writes the data to a temporary file on disk.
            """
            if self._file is None:
                self._file = self._ref()._build_file_name()
                openmode = self._openmode(self.data)
                kwargs = {} if 'b' in openmode else {'encoding': 'utf8'}
                with open(self._file, openmode, **kwargs) as fh:
                    if isinstance(self.data, (bytes, str)):
                        fh.write(self.data)
                    else:
                        json.dump(self.data, fh)

            return self._file

        @property
        def stream(self):
            """Return a stream representation of the data."""
            if isinstance(self.data, bytes):
                return BytesIO(self.data)
            else:
                return StringIO(str(self.data))

    class StreamHandler(DataHandler):
        prop = 'stream'

        def __init__(self, value, ref):
            self._ref = weakref.ref(ref)
            self._file = None
            self._data = None
            self._stream = value

        @property
        def stream(self):
            """Return the stream."""
            return self._stream

        @property
        def data(self):
            """Return the data from the stream."""
            if self._data is None:
                self._data = self.stream.read()
            return self._data

    class UrlHandler(FileHandler):
        prop = 'url'

        def __init__(self, value, ref):
            self._ref = weakref.ref(ref)
            self._file = None
            self._data = None
            self._stream = None
            self._url = value
            self._post_data = None

        @property
        def url(self):
            """Return the URL."""
            return self._url

        @property
        def file(self):
            """Downloads URL and return file pointer.
            Checks if size is allowed before download.
            """
            if self._file is not None:
                return self._file

            self._file = self._ref()._build_file_name(href=self.url)

            max_byte_size = self.max_size()

            # Create request
            try:
                reference_file = self._openurl(self.url, self.post_data)
                data_size = reference_file.headers.get('Content-Length', 0)
            except Exception as e:
                raise NoApplicableCode('File reference error: {}'.format(e))

            error_message = 'File size for input "{}" exceeded. Maximum allowed: {}'.format(
                self._ref().inpt.get('identifier', '?'), humanize.naturalsize(max_byte_size))

            if int(max_byte_size) > 0:
                if int(data_size) > int(max_byte_size):
                    raise FileSizeExceeded(error_message)

            try:
                with open(self._file, 'wb') as f:
                    data_size = 0
                    for chunk in reference_file.iter_content(chunk_size=1024):
                        data_size += len(chunk)
                        if int(max_byte_size) > 0:
                            if int(data_size) > int(max_byte_size):
                                raise FileSizeExceeded(error_message)
                        f.write(chunk)
            except FileSizeExceeded:
                raise
            except Exception as e:
                raise NoApplicableCode(e)

            return self._file

        @property
        def post_data(self):
            return self._post_data

        @post_data.setter
        def post_data(self, value):
            self._post_data = value

        @property
        def size(self):
            """Get content-length of URL without download"""
            req = self._openurl(self.url)
            if req.ok:
                size = int(req.headers.get('content-length', '0'))
            else:
                size = 0
            return size

        @staticmethod
        def _openurl(href, data=None):
            """Open given href.
            """
            LOGGER.debug('Fetching URL {}'.format(href))
            if data is not None:
                req = requests.post(url=href, data=data, stream=True)
            else:
                req = requests.get(url=href, stream=True)

            return req

        @staticmethod
        def max_size():
            """Calculates maximal size for input file based on configuration
            and units.

            :return: maximum file size in bytes
            """
            ms = config.get_config_value('server', 'maxsingleinputsize')
            byte_size = config.get_size_mb(ms) * 1024**2
            return byte_size

    class SimpleHandler(IOHandler):
        """Data handler for Literal In- and Outputs

        >>> class Int_type(object):
        ...     @staticmethod
        ...     def convert(value): return int(value)
        >>>
        >>> class MyValidator(object):
        ...     @staticmethod
        ...     def validate(inpt): return 0 < inpt.data < 3
        >>>
        >>> inpt = SimpleHandler(data_type = Int_type)
        >>> inpt.validator = MyValidator
        >>>
        >>> inpt.data = 1
        >>> inpt.validator.validate(inpt)
        True
        >>> inpt.data = 5
        >>> inpt.validator.validate(inpt)
        False
        """

        def __init__(self, workdir=None, data_type=None, mode=MODE.NONE):
            IOHandler.__init__(self, workdir=workdir, mode=mode)
            if data_type not in LITERAL_DATA_TYPES:
                raise ValueError('data_type {} not in {}'.format(data_type, LITERAL_DATA_TYPES))
            self.data_type = data_type

        @IOHandler.data.setter
        def data(self, value):
            """Set data value. Inputs are converted into target format.
            """
            if self.data_type and value is not None:
                value = convert(self.data_type, value)

            IOHandler.data.fset(self, value)

    class BasicIO:
        """Basic Input/Output class
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=None,
                     min_occurs=1, max_occurs=1, metadata=[], translations=None):
            self.identifier = identifier
            self.title = title
            self.abstract = abstract
            self.keywords = keywords
            self.min_occurs = int(min_occurs) if min_occurs is not None else 0
            self.max_occurs = int(max_occurs) if max_occurs is not None else None
            self.metadata = metadata
            self.translations = lower_case_dict(translations)

    class BasicLiteral:
        """Basic literal Input/Output class
        """

        def __init__(self, data_type="integer", uoms=None):
            assert data_type in LITERAL_DATA_TYPES
            self.data_type = data_type
            # list of uoms
            self.uoms = []
            # current uom
            self._uom = None

            # add all uoms (upcasting to UOM)
            if uoms is not None:
                for uom in uoms:
                    if not isinstance(uom, UOM):
                        uom = UOM(uom)
                    self.uoms.append(uom)

            if self.uoms:
                # default/current uom
                self.uom = self.uoms[0]

        @property
        def uom(self):
            return self._uom

        @uom.setter
        def uom(self, uom):
            if uom is not None:
                self._uom = uom

    class BasicComplex(object):
        """Basic complex input/output class

        """

        def __init__(self, data_format=None, supported_formats=None):
            self._data_format = data_format
            self._supported_formats = ()
            if supported_formats:
                self.supported_formats = supported_formats

            if data_format:
                self.data_format = data_format
            elif self.supported_formats:
                # not an empty list, set the default/current format to the first
                self.data_format = supported_formats[0]

        def get_format(self, mime_type):
            """
            :param mime_type: given mimetype
            :return: Format
            """

            for frmt in self.supported_formats:
                if frmt.mime_type == mime_type:
                    return frmt
            else:
                return None

        @property
        def validator(self):
            """Return the proper validator for given data_format
            """
            return None if self.data_format is None else self.data_format.validate

        @property
        def supported_formats(self):
            return self._supported_formats

        @supported_formats.setter
        def supported_formats(self, supported_formats):
            """Setter of supported formats
            """

            def set_format_validator(supported_format):
                if not supported_format.validate or \
                   supported_format.validate == emptyvalidator:
                    supported_format.validate =\
                        get_validator(supported_format.mime_type)
                return supported_format

            self._supported_formats = tuple(map(set_format_validator, supported_formats))

        @property
        def data_format(self):
            return self._data_format

        @data_format.setter
        def data_format(self, data_format):
            """self data_format setter
            """
            if self._is_supported(data_format):
                self._data_format = data_format
                if not data_format.validate or data_format.validate == emptyvalidator:
                    data_format.validate = get_validator(data_format.mime_type)
            else:
                raise InvalidParameterValue("Requested format {}, {}, {} not supported".format(
                                            data_format.mime_type,
                                            data_format.encoding,
                                            data_format.schema),
                                            'mimeType')

        def _is_supported(self, data_format):

            if self.supported_formats:
                for frmt in self.supported_formats:
                    if frmt.same_as(data_format):
                        return True

            return False

    class BasicBoundingBox(object):
        """Basic BoundingBox input/output class
        """

        def __init__(self, crss=None, dimensions=2):
            self._data = None
            self.crss = crss or ['epsg:4326']
            self.crs = self.crss[0]
            self.dimensions = dimensions

        @property
        def data(self):
            return self._data

        @data.setter
        def data(self, value):
            if isinstance(value, list):
                self._data = [float(number) for number in value]
            elif isinstance(value, str):
                self._data = [float(number) for number in value.split(',')[:4]]
            else:
                self._data = None

        @property
        def ll(self):
            if self.data:
                return self.data[:2]
            return []

        @property
        def ur(self):
            if self.data:
                return self.data[2:]
            return []

    class LiteralInput(BasicIO, BasicLiteral, SimpleHandler):
        """LiteralInput input abstract class
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=None,
                     data_type="integer", workdir=None, allowed_values=None,
                     uoms=None, mode=MODE.NONE,
                     min_occurs=1, max_occurs=1, metadata=[],
                     default=None, default_type=SOURCE_TYPE.DATA, translations=None):
            BasicIO.__init__(self,
                             identifier=identifier,
                             title=title,
                             abstract=abstract,
                             keywords=keywords,
                             min_occurs=min_occurs,
                             max_occurs=max_occurs,
                             metadata=metadata,
                             translations=translations,
                             )
            BasicLiteral.__init__(self, data_type, uoms)
            SimpleHandler.__init__(self, workdir, data_type, mode=mode)

            if default_type != SOURCE_TYPE.DATA:
                raise InvalidParameterValue("Source types other than data are not supported.")

            self.any_value = False
            self.values_reference = None
            self.allowed_values = []

            if allowed_values:
                if not isinstance(allowed_values, (tuple, list)):
                    allowed_values = [allowed_values]
                self.any_value = any(is_anyvalue(a) for a in allowed_values)
                for value in allowed_values:
                    if is_values_reference(value):
                        self.values_reference = value
                        break
                self.allowed_values = make_allowedvalues(allowed_values)

            self._default = default
            self._default_type = default_type

            if default is not None:
                self.data = default

        @property
        def validator(self):
            """Get validator for any value as well as allowed_values
            :rtype: function
            """

            if self.any_value:
                return validate_anyvalue
            elif self.values_reference:
                return validate_values_reference
            elif self.allowed_values:
                return validate_allowed_values
            else:
                return validate_value

    class LiteralOutput(BasicIO, BasicLiteral, SimpleHandler):
        """Basic LiteralOutput class
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=None,
                     data_type=None, workdir=None, uoms=None, validate=None,
                     mode=MODE.NONE, translations=None):
            BasicIO.__init__(self, identifier, title, abstract, keywords, translations=translations)
            BasicLiteral.__init__(self, data_type, uoms)
            SimpleHandler.__init__(self, workdir=None, data_type=data_type,
                                   mode=mode)

            self._storage = None

        @property
        def storage(self):
            return self._storage

        @storage.setter
        def storage(self, storage):
            self._storage = storage

        @property
        def validator(self):
            """Get validator for any value as well as allowed_values
            """

            return validate_anyvalue

    class BBoxInput(BasicIO, BasicBoundingBox, IOHandler):
        """Basic Bounding box input abstract class
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=[], crss=None,
                     dimensions=None, workdir=None,
                     mode=MODE.SIMPLE,
                     min_occurs=1, max_occurs=1, metadata=[],
                     default=None, default_type=SOURCE_TYPE.DATA, translations=None):
            BasicIO.__init__(self,
                             identifier=identifier,
                             title=title,
                             abstract=abstract,
                             keywords=keywords,
                             min_occurs=min_occurs,
                             max_occurs=max_occurs,
                             metadata=metadata,
                             translations=translations,
                             )
            BasicBoundingBox.__init__(self, crss, dimensions)
            IOHandler.__init__(self, workdir=workdir, mode=mode)

            if default_type != SOURCE_TYPE.DATA:
                raise InvalidParameterValue("Source types other than data are not supported.")

            self._default = default
            self._default_type = default_type

            self._set_default_value(default, default_type)

    class BBoxOutput(BasicIO, BasicBoundingBox, IOHandler):
        """Basic BoundingBox output class
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=None, crss=None,
                     dimensions=None, workdir=None, mode=MODE.NONE, translations=None):
            BasicIO.__init__(self, identifier, title, abstract, keywords, translations=translations)
            BasicBoundingBox.__init__(self, crss, dimensions)
            IOHandler.__init__(self, workdir=workdir, mode=mode)
            self._storage = None

        @property
        def storage(self):
            return self._storage

        @storage.setter
        def storage(self, storage):
            self._storage = storage

    class ComplexInput(BasicIO, BasicComplex, IOHandler):
        """Complex input abstract class

        >>> ci = ComplexInput()
        >>> ci.validator = 1
        >>> ci.validator
        1
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=None,
                     workdir=None, data_format=None, supported_formats=None,
                     mode=MODE.NONE,
                     min_occurs=1, max_occurs=1, metadata=[],
                     default=None, default_type=SOURCE_TYPE.DATA, translations=None):
            BasicIO.__init__(self,
                             identifier=identifier,
                             title=title,
                             abstract=abstract,
                             keywords=keywords,
                             min_occurs=min_occurs,
                             max_occurs=max_occurs,
                             metadata=metadata,
                             translations=translations,
                             )
            IOHandler.__init__(self, workdir=workdir, mode=mode)
            BasicComplex.__init__(self, data_format, supported_formats)

            self._default = default
            self._default_type = default_type

        def file_handler(self, inpt):
            """<wps:Reference /> handler.
            Used when href is a file url."""
            # check if file url is allowed
            self._validate_file_input(href=inpt.get('href'))
            # save the file reference input in workdir
            tmp_file = self._build_file_name(href=inpt.get('href'))

            try:
                inpt_file = urlparse(inpt.get('href')).path
                inpt_file = os.path.abspath(inpt_file)
                os.symlink(inpt_file, tmp_file)
                LOGGER.debug("Linked input file {} to {}.".format(inpt_file, tmp_file))
            except Exception:
                # TODO: handle os.symlink on windows
                # raise NoApplicableCode("Could not link file reference: {}".format(e))
                LOGGER.warning("Could not link file reference")
                shutil.copy2(inpt_file, tmp_file)

            return tmp_file

        def url_handler(self, inpt):
            # That could possibly go into the data property...
            if inpt.get('method') == 'POST':
                if 'body' in inpt:
                    self.post_data = inpt.get('body')
                elif 'bodyreference' in inpt:
                    self.post_data = requests.get(url=inpt.get('bodyreference')).text
                else:
                    raise AttributeError("Missing post data content.")

            return inpt.get('href')

        def process(self, inpt):
            """Subclass with the appropriate handler given the data input."""
            href = inpt.get('href', None)
            self.inpt = inpt

            if href:
                if urlparse(href).scheme == 'file':
                    self.file = self.file_handler(inpt)

                else:
                    # No file download occurs here. The file content will
                    # only be retrieved when the file property is accessed.
                    self.url = self.url_handler(inpt)

            else:
                self.data = inpt.get('data')

        @staticmethod
        def _validate_file_input(href):
            href = href or ''
            parsed_url = urlparse(href)
            if parsed_url.scheme != 'file':
                raise FileURLNotSupported('Invalid URL scheme')
            file_path = parsed_url.path
            if not file_path:
                raise FileURLNotSupported('Invalid URL path')
            file_path = os.path.abspath(file_path)
            # build allowed paths list
            inputpaths = config.get_config_value('server', 'allowedinputpaths')
            allowed_paths = [os.path.abspath(p.strip()) for p in inputpaths.split(os.pathsep) if p.strip()]
            for allowed_path in allowed_paths:
                if file_path.startswith(allowed_path):
                    LOGGER.debug("Accepted file url as input.")
                    return
            raise FileURLNotSupported()

    class ComplexOutput(BasicIO, BasicComplex, IOHandler):
        """Complex output abstract class

        >>> # temporary configuration
        >>> import ConfigParser
        >>> from pywps.storage import *
        >>> config = ConfigParser.RawConfigParser()
        >>> config.add_section('FileStorage')
        >>> config.set('FileStorage', 'target', './')
        >>> config.add_section('server')
        >>> config.set('server', 'outputurl', 'http://foo/bar/filestorage')
        >>>
        >>> # create temporary file
        >>> tiff_file = open('file.tiff', 'w')
        >>> tiff_file.write("AA")
        >>> tiff_file.close()
        >>>
        >>> co = ComplexOutput()
        >>> co.file ='file.tiff'
        >>> fs = FileStorage(config)
        >>> co.storage = fs
        >>>
        >>> url = co.url # get url, data are stored
        >>>
        >>> co.stream.read() # get data - nothing is stored
        'AA'
        """

        def __init__(self, identifier, title=None, abstract=None, keywords=None,
                     workdir=None, data_format=None, supported_formats: Supported_Formats = None,
                     mode=MODE.NONE, translations: Translations = None):
            BasicIO.__init__(self, identifier, title, abstract, keywords, translations=translations)
            IOHandler.__init__(self, workdir=workdir, mode=mode)
            BasicComplex.__init__(self, data_format, supported_formats)

            self._storage = None

        @property
        def storage(self):
            return self._storage

        @storage.setter
        def storage(self, storage):
            # don't set storage twice
            if self._storage is None:
                self._storage = storage

        # TODO: refactor ?
        def get_url(self):
            """Return URL pointing to data
            """
            # TODO: it is not obvious that storing happens here
            (_, _, url) = self.storage.store(self)
            # url = self.storage.url(self)
            return url