diff --git a/search/cmd/search/main.go b/search/cmd/search/main.go
new file mode 100644
index 0000000000..71f1420618
--- /dev/null
+++ b/search/cmd/search/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "os"
+
+ "github.com/owncloud/ocis/search/pkg/command"
+ "github.com/owncloud/ocis/search/pkg/config/defaults"
+)
+
+func main() {
+ if err := command.Execute(defaults.DefaultConfig()); err != nil {
+ os.Exit(1)
+ }
+}
diff --git a/webdav/pkg/errors/error.go b/webdav/pkg/errors/error.go
new file mode 100644
index 0000000000..ba7f9b908a
--- /dev/null
+++ b/webdav/pkg/errors/error.go
@@ -0,0 +1,170 @@
+package errors
+
+import (
+ "bytes"
+ "encoding/xml"
+ "net/http"
+
+ rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
+ "github.com/cs3org/reva/v2/pkg/rgrpc/status"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+var sabreException = map[int]string{
+
+ // the commented states have no corresponding exception in sabre/dav,
+ // see https://github.com/sabre-io/dav/tree/master/lib/DAV/Exception
+
+ // http.StatusMultipleChoices: "Multiple Choices",
+ // http.StatusMovedPermanently: "Moved Permanently",
+ // http.StatusFound: "Found",
+ // http.StatusSeeOther: "See Other",
+ // http.StatusNotModified: "Not Modified",
+ // http.StatusUseProxy: "Use Proxy",
+ // http.StatusTemporaryRedirect: "Temporary Redirect",
+ // http.StatusPermanentRedirect: "Permanent Redirect",
+
+ http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest",
+ http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated",
+ http.StatusPaymentRequired: "Sabre\\DAV\\Exception\\PaymentRequired",
+ http.StatusForbidden: "Sabre\\DAV\\Exception\\Forbidden", // InvalidResourceType, InvalidSyncToken, TooManyMatches
+ http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound",
+ http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed",
+ // http.StatusNotAcceptable: "Not Acceptable",
+ // http.StatusProxyAuthRequired: "Proxy Authentication Required",
+ // http.StatusRequestTimeout: "Request Timeout",
+ http.StatusConflict: "Sabre\\DAV\\Exception\\Conflict", // LockTokenMatchesRequestUri
+ // http.StatusGone: "Gone",
+ http.StatusLengthRequired: "Sabre\\DAV\\Exception\\LengthRequired",
+ http.StatusPreconditionFailed: "Sabre\\DAV\\Exception\\PreconditionFailed",
+ // http.StatusRequestEntityTooLarge: "Request Entity Too Large",
+ // http.StatusRequestURITooLong: "Request URI Too Long",
+ http.StatusUnsupportedMediaType: "Sabre\\DAV\\Exception\\UnsupportedMediaType", // ReportNotSupported
+ http.StatusRequestedRangeNotSatisfiable: "Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable",
+ // http.StatusExpectationFailed: "Expectation Failed",
+ // http.StatusTeapot: "I'm a teapot",
+ // http.StatusMisdirectedRequest: "Misdirected Request",
+ // http.StatusUnprocessableEntity: "Unprocessable Entity",
+ http.StatusLocked: "Sabre\\DAV\\Exception\\Locked", // ConflictingLock
+ // http.StatusFailedDependency: "Failed Dependency",
+ // http.StatusTooEarly: "Too Early",
+ // http.StatusUpgradeRequired: "Upgrade Required",
+ // http.StatusPreconditionRequired: "Precondition Required",
+ // http.StatusTooManyRequests: "Too Many Requests",
+ // http.StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large",
+ // http.StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons",
+
+ // http.StatusInternalServerError: "Internal Server Error",
+ http.StatusNotImplemented: "Sabre\\DAV\\Exception\\NotImplemented",
+ // http.StatusBadGateway: "Bad Gateway",
+ http.StatusServiceUnavailable: "Sabre\\DAV\\Exception\\ServiceUnavailable",
+ // http.StatusGatewayTimeout: "Gateway Timeout",
+ // http.StatusHTTPVersionNotSupported: "HTTP Version Not Supported",
+ // http.StatusVariantAlsoNegotiates: "Variant Also Negotiates",
+ http.StatusInsufficientStorage: "Sabre\\DAV\\Exception\\InsufficientStorage",
+ // http.StatusLoopDetected: "Loop Detected",
+ // http.StatusNotExtended: "Not Extended",
+ // http.StatusNetworkAuthenticationRequired: "Network Authentication Required",
+}
+
+// SabreException returns a sabre exception text for the HTTP status code. It returns the empty
+// string if the code is unknown.
+func SabreException(code int) string {
+ return sabreException[code]
+}
+
+// Exception represents a ocdav exception
+type Exception struct {
+ Code int
+ Message string
+ Header string
+}
+
+// Marshal just calls the xml marshaller for a given exception.
+func Marshal(code int, message string, header string) ([]byte, error) {
+ xmlstring, err := xml.Marshal(&ErrorXML{
+ Xmlnsd: "DAV",
+ Xmlnss: "http://sabredav.org/ns",
+ Exception: sabreException[code],
+ Message: message,
+ Header: header,
+ })
+ if err != nil {
+ return nil, err
+ }
+ var buf bytes.Buffer
+ buf.WriteString(xml.Header)
+ buf.Write(xmlstring)
+ return buf.Bytes(), err
+}
+
+// ErrorXML holds the xml representation of an error
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
+type ErrorXML struct {
+ XMLName xml.Name `xml:"d:error"`
+ Xmlnsd string `xml:"xmlns:d,attr"`
+ Xmlnss string `xml:"xmlns:s,attr"`
+ Exception string `xml:"s:exception"`
+ Message string `xml:"s:message"`
+ InnerXML []byte `xml:",innerxml"`
+ // Header is used to indicate the conflicting request header
+ Header string `xml:"s:header,omitempty"`
+}
+
+var (
+ // ErrInvalidDepth is an invalid depth header error
+ ErrInvalidDepth = errors.New("webdav: invalid depth")
+ // ErrInvalidPropfind is an invalid propfind error
+ ErrInvalidPropfind = errors.New("webdav: invalid propfind")
+ // ErrInvalidProppatch is an invalid proppatch error
+ ErrInvalidProppatch = errors.New("webdav: invalid proppatch")
+ // ErrInvalidLockInfo is an invalid lock error
+ ErrInvalidLockInfo = errors.New("webdav: invalid lock info")
+ // ErrUnsupportedLockInfo is an unsupported lock error
+ ErrUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
+ // ErrInvalidTimeout is an invalid timeout error
+ ErrInvalidTimeout = errors.New("webdav: invalid timeout")
+ // ErrInvalidIfHeader is an invalid if header error
+ ErrInvalidIfHeader = errors.New("webdav: invalid If header")
+ // ErrUnsupportedMethod is an unsupported method error
+ ErrUnsupportedMethod = errors.New("webdav: unsupported method")
+ // ErrInvalidLockToken is an invalid lock token error
+ ErrInvalidLockToken = errors.New("webdav: invalid lock token")
+ // ErrConfirmationFailed is returned by a LockSystem's Confirm method.
+ ErrConfirmationFailed = errors.New("webdav: confirmation failed")
+ // ErrForbidden is returned by a LockSystem's Unlock method.
+ ErrForbidden = errors.New("webdav: forbidden")
+ // ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods.
+ ErrLocked = errors.New("webdav: locked")
+ // ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.
+ ErrNoSuchLock = errors.New("webdav: no such lock")
+ // ErrNotImplemented is returned when hitting not implemented code paths
+ ErrNotImplemented = errors.New("webdav: not implemented")
+)
+
+// HandleErrorStatus checks the status code, logs a Debug or Error level message
+// and writes an appropriate http status
+func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) {
+ hsc := status.HTTPStatusFromCode(s.Code)
+ if hsc == http.StatusInternalServerError {
+ log.Error().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc))
+ } else {
+ log.Debug().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc))
+ }
+ w.WriteHeader(hsc)
+}
+
+// HandleWebdavError checks the status code, logs an error and creates a webdav response body
+// if needed
+func HandleWebdavError(log *zerolog.Logger, w http.ResponseWriter, b []byte, err error) {
+ if err != nil {
+ log.Error().Msgf("error marshaling xml response: %s", b)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ _, err = w.Write(b)
+ if err != nil {
+ log.Err(err).Msg("error writing response")
+ }
+}
diff --git a/webdav/pkg/net/headers.go b/webdav/pkg/net/headers.go
new file mode 100644
index 0000000000..5c22d8509d
--- /dev/null
+++ b/webdav/pkg/net/headers.go
@@ -0,0 +1,48 @@
+package net
+
+// Common HTTP headers.
+const (
+ HeaderAcceptRanges = "Accept-Ranges"
+ HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers"
+ HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers"
+ HeaderContentDisposistion = "Content-Disposition"
+ HeaderContentLength = "Content-Length"
+ HeaderContentRange = "Content-Range"
+ HeaderContentType = "Content-Type"
+ HeaderETag = "ETag"
+ HeaderLastModified = "Last-Modified"
+ HeaderLocation = "Location"
+ HeaderRange = "Range"
+ HeaderIfMatch = "If-Match"
+)
+
+// webdav headers
+const (
+ HeaderDav = "DAV" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.1
+ HeaderDepth = "Depth" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.2
+ HeaderDestination = "Destination" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.3
+ HeaderIf = "If" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.4
+ HeaderLockToken = "Lock-Token" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.5
+ HeaderOverwrite = "Overwrite" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.6
+ HeaderTimeout = "Timeout" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.7
+)
+
+// Non standard HTTP headers.
+const (
+ HeaderOCFileID = "OC-FileId"
+ HeaderOCETag = "OC-ETag"
+ HeaderOCChecksum = "OC-Checksum"
+ HeaderOCPermissions = "OC-Perm"
+ HeaderTusResumable = "Tus-Resumable"
+ HeaderTusVersion = "Tus-Version"
+ HeaderTusExtension = "Tus-Extension"
+ HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm"
+ HeaderTusUploadExpires = "Upload-Expires"
+ HeaderUploadChecksum = "Upload-Checksum"
+ HeaderUploadLength = "Upload-Length"
+ HeaderUploadMetadata = "Upload-Metadata"
+ HeaderUploadOffset = "Upload-Offset"
+ HeaderOCMtime = "X-OC-Mtime"
+ HeaderExpectedEntityLength = "X-Expected-Entity-Length"
+ HeaderLitmus = "X-Litmus"
+)
diff --git a/webdav/pkg/net/net.go b/webdav/pkg/net/net.go
new file mode 100644
index 0000000000..6ac66b74a6
--- /dev/null
+++ b/webdav/pkg/net/net.go
@@ -0,0 +1,12 @@
+package net
+
+import (
+ "net/url"
+)
+
+// EncodePath encodes the path of a url.
+//
+// slashes (/) are treated as path-separators.
+func EncodePath(path string) string {
+ return (&url.URL{Path: path}).EscapedPath()
+}
diff --git a/webdav/pkg/prop/prop.go b/webdav/pkg/prop/prop.go
new file mode 100644
index 0000000000..b34ce19fe8
--- /dev/null
+++ b/webdav/pkg/prop/prop.go
@@ -0,0 +1,125 @@
+package prop
+
+import (
+ "bytes"
+ "encoding/xml"
+)
+
+// PropertyXML represents a single DAV resource property as defined in RFC 4918.
+// http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
+type PropertyXML struct {
+ // XMLName is the fully qualified name that identifies this property.
+ XMLName xml.Name
+
+ // Lang is an optional xml:lang attribute.
+ Lang string `xml:"xml:lang,attr,omitempty"`
+
+ // InnerXML contains the XML representation of the property value.
+ // See http://www.webdav.org/specs/rfc4918.html#property_values
+ //
+ // Property values of complex type or mixed-content must have fully
+ // expanded XML namespaces or be self-contained with according
+ // XML namespace declarations. They must not rely on any XML
+ // namespace declarations within the scope of the XML document,
+ // even including the DAV: namespace.
+ InnerXML []byte `xml:",innerxml"`
+}
+
+func xmlEscaped(val string) []byte {
+ buf := new(bytes.Buffer)
+ xml.Escape(buf, []byte(val))
+ return buf.Bytes()
+}
+
+// EscapedNS returns a new PropertyXML instance while xml-escaping the value
+func EscapedNS(namespace string, local string, val string) PropertyXML {
+ return PropertyXML{
+ XMLName: xml.Name{Space: namespace, Local: local},
+ Lang: "",
+ InnerXML: xmlEscaped(val),
+ }
+}
+
+// Escaped returns a new PropertyXML instance while xml-escaping the value
+// TODO properly use the space
+func Escaped(key, val string) PropertyXML {
+ return PropertyXML{
+ XMLName: xml.Name{Space: "", Local: key},
+ Lang: "",
+ InnerXML: xmlEscaped(val),
+ }
+}
+
+// NotFound returns a new PropertyXML instance with an empty value
+func NotFound(key string) PropertyXML {
+ return PropertyXML{
+ XMLName: xml.Name{Space: "", Local: key},
+ Lang: "",
+ }
+}
+
+// NotFoundNS returns a new PropertyXML instance with the given namespace and an empty value
+func NotFoundNS(namespace, key string) PropertyXML {
+ return PropertyXML{
+ XMLName: xml.Name{Space: namespace, Local: key},
+ Lang: "",
+ }
+}
+
+// Raw returns a new PropertyXML instance for the given key/value pair
+// TODO properly use the space
+func Raw(key, val string) PropertyXML {
+ return PropertyXML{
+ XMLName: xml.Name{Space: "", Local: key},
+ Lang: "",
+ InnerXML: []byte(val),
+ }
+}
+
+// Next returns the next token, if any, in the XML stream of d.
+// RFC 4918 requires to ignore comments, processing instructions
+// and directives.
+// http://www.webdav.org/specs/rfc4918.html#property_values
+// http://www.webdav.org/specs/rfc4918.html#xml-extensibility
+func Next(d *xml.Decoder) (xml.Token, error) {
+ for {
+ t, err := d.Token()
+ if err != nil {
+ return t, err
+ }
+ switch t.(type) {
+ case xml.Comment, xml.Directive, xml.ProcInst:
+ continue
+ default:
+ return t, nil
+ }
+ }
+}
+
+// ActiveLock holds active lock xml data
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_activelock
+//
+type ActiveLock struct {
+ XMLName xml.Name `xml:"activelock"`
+ Exclusive *struct{} `xml:"lockscope>exclusive,omitempty"`
+ Shared *struct{} `xml:"lockscope>shared,omitempty"`
+ Write *struct{} `xml:"locktype>write,omitempty"`
+ Depth string `xml:"depth"`
+ Owner Owner `xml:"owner,omitempty"`
+ Timeout string `xml:"timeout,omitempty"`
+ Locktoken string `xml:"locktoken>href"`
+ Lockroot string `xml:"lockroot>href,omitempty"`
+}
+
+// Owner captures the inner UML of a lock owner element http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
+type Owner struct {
+ InnerXML string `xml:",innerxml"`
+}
+
+// Escape repaces ", &, ', < and > with their xml representation
+func Escape(s string) string {
+ b := bytes.NewBuffer(nil)
+ _ = xml.EscapeText(b, []byte(s))
+ return b.String()
+}
diff --git a/webdav/pkg/propfind/propfind.go b/webdav/pkg/propfind/propfind.go
new file mode 100644
index 0000000000..5a2f551cb5
--- /dev/null
+++ b/webdav/pkg/propfind/propfind.go
@@ -0,0 +1,180 @@
+package propfind
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/owncloud/ocis/webdav/pkg/errors"
+ "github.com/owncloud/ocis/webdav/pkg/prop"
+)
+
+const (
+ _spaceTypeProject = "project"
+)
+
+type countingReader struct {
+ n int
+ r io.Reader
+}
+
+func (c *countingReader) Read(p []byte) (int, error) {
+ n, err := c.r.Read(p)
+ c.n += n
+ return n, err
+}
+
+// Props represents properties related to a resource
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)
+type Props []xml.Name
+
+// XML holds the xml representation of a propfind
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind
+type XML struct {
+ XMLName xml.Name `xml:"DAV: propfind"`
+ Allprop *struct{} `xml:"DAV: allprop"`
+ Propname *struct{} `xml:"DAV: propname"`
+ Prop Props `xml:"DAV: prop"`
+ Include Props `xml:"DAV: include"`
+}
+
+// PropstatXML holds the xml representation of a propfind response
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
+type PropstatXML struct {
+ // Prop requires DAV: to be the default namespace in the enclosing
+ // XML. This is due to the standard encoding/xml package currently
+ // not honoring namespace declarations inside a xmltag with a
+ // parent element for anonymous slice elements.
+ // Use of multistatusWriter takes care of this.
+ Prop []prop.PropertyXML `xml:"d:prop>_ignored_"`
+ Status string `xml:"d:status"`
+ Error *errors.ErrorXML `xml:"d:error"`
+ ResponseDescription string `xml:"d:responsedescription,omitempty"`
+}
+
+// ResponseXML holds the xml representation of a propfind response
+type ResponseXML struct {
+ XMLName xml.Name `xml:"d:response"`
+ Href string `xml:"d:href"`
+ Propstat []PropstatXML `xml:"d:propstat"`
+ Status string `xml:"d:status,omitempty"`
+ Error *errors.ErrorXML `xml:"d:error"`
+ ResponseDescription string `xml:"d:responsedescription,omitempty"`
+}
+
+// MultiStatusResponseXML holds the xml representation of a multistatus propfind response
+type MultiStatusResponseXML struct {
+ XMLName xml.Name `xml:"d:multistatus"`
+ XmlnsS string `xml:"xmlns:s,attr,omitempty"`
+ XmlnsD string `xml:"xmlns:d,attr,omitempty"`
+ XmlnsOC string `xml:"xmlns:oc,attr,omitempty"`
+
+ Responses []*ResponseXML `xml:"d:response"`
+}
+
+// ResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400
+type ResponseUnmarshalXML struct {
+ XMLName xml.Name `xml:"response"`
+ Href string `xml:"href"`
+ Propstat []PropstatUnmarshalXML `xml:"propstat"`
+ Status string `xml:"status,omitempty"`
+ Error *errors.ErrorXML `xml:"d:error"`
+ ResponseDescription string `xml:"responsedescription,omitempty"`
+}
+
+// MultiStatusResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400
+type MultiStatusResponseUnmarshalXML struct {
+ XMLName xml.Name `xml:"multistatus"`
+ XmlnsS string `xml:"xmlns:s,attr,omitempty"`
+ XmlnsD string `xml:"xmlns:d,attr,omitempty"`
+ XmlnsOC string `xml:"xmlns:oc,attr,omitempty"`
+
+ Responses []*ResponseUnmarshalXML `xml:"response"`
+}
+
+// PropstatUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400
+type PropstatUnmarshalXML struct {
+ // Prop requires DAV: to be the default namespace in the enclosing
+ // XML. This is due to the standard encoding/xml package currently
+ // not honoring namespace declarations inside a xmltag with a
+ // parent element for anonymous slice elements.
+ // Use of multistatusWriter takes care of this.
+ Prop []*prop.PropertyXML `xml:"prop"`
+ Status string `xml:"status"`
+ Error *errors.ErrorXML `xml:"d:error"`
+ ResponseDescription string `xml:"responsedescription,omitempty"`
+}
+
+// NewMultiStatusResponseXML returns a preconfigured instance of MultiStatusResponseXML
+func NewMultiStatusResponseXML() *MultiStatusResponseXML {
+ return &MultiStatusResponseXML{
+ XmlnsD: "DAV:",
+ XmlnsS: "http://sabredav.org/ns",
+ XmlnsOC: "http://owncloud.org/ns",
+ }
+}
+
+// ReadPropfind extracts and parses the propfind XML information from a Reader
+// from https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/xml.go#L178-L205
+func ReadPropfind(r io.Reader) (pf XML, status int, err error) {
+ c := countingReader{r: r}
+ if err = xml.NewDecoder(&c).Decode(&pf); err != nil {
+ if err == io.EOF {
+ if c.n == 0 {
+ // An empty body means to propfind allprop.
+ // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
+ return XML{Allprop: new(struct{})}, 0, nil
+ }
+ err = errors.ErrInvalidPropfind
+ }
+ return XML{}, http.StatusBadRequest, err
+ }
+
+ if pf.Allprop == nil && pf.Include != nil {
+ return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind
+ }
+ if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) {
+ return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind
+ }
+ if pf.Prop != nil && pf.Propname != nil {
+ return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind
+ }
+ if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil {
+ // jfd: I think is perfectly valid ... treat it as allprop
+ return XML{Allprop: new(struct{})}, 0, nil
+ }
+ return pf, 0, nil
+}
+
+// UnmarshalXML appends the property names enclosed within start to pn.
+//
+// It returns an error if start does not contain any properties or if
+// properties contain values. Character data between properties is ignored.
+func (pn *Props) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ for {
+ t, err := prop.Next(d)
+ if err != nil {
+ return err
+ }
+ switch e := t.(type) {
+ case xml.EndElement:
+ // jfd: I think is perfectly valid ... treat it as allprop
+ /*
+ if len(*pn) == 0 {
+ return fmt.Errorf("%s must not be empty", start.Name.Local)
+ }
+ */
+ return nil
+ case xml.StartElement:
+ t, err = prop.Next(d)
+ if err != nil {
+ return err
+ }
+ if _, ok := t.(xml.EndElement); !ok {
+ return fmt.Errorf("unexpected token %T", t)
+ }
+ *pn = append(*pn, e.Name)
+ }
+ }
+}