From 087b22bcf3005cc698ae954d469b536746e0916b Mon Sep 17 00:00:00 2001 From: Russell Horton Date: Fri, 11 Nov 2011 01:06:12 -0800 Subject: [PATCH] Added templates and config for Python client generation. --- bin/generate-python-lib.sh | 23 ++ conf/python/structure/APIClient.py | 128 ++++++++++ conf/python/structure/__init__.py | 7 + conf/python/templates/EnumObject.st | 25 ++ conf/python/templates/ModelObject.st | 37 +++ conf/python/templates/ResourceObject.st | 115 +++++++++ conf/python/templates/VersionChecker.st | 36 +++ .../python/PythonDataTypeMappingProvider.java | 228 ++++++++++++++++++ .../config/python/PythonLibCodeGen.java | 103 ++++++++ 9 files changed, 702 insertions(+) create mode 100755 bin/generate-python-lib.sh create mode 100644 conf/python/structure/APIClient.py create mode 100644 conf/python/structure/__init__.py create mode 100644 conf/python/templates/EnumObject.st create mode 100644 conf/python/templates/ModelObject.st create mode 100644 conf/python/templates/ResourceObject.st create mode 100644 conf/python/templates/VersionChecker.st create mode 100644 src/main/java/com/wordnik/swagger/codegen/config/python/PythonDataTypeMappingProvider.java create mode 100644 src/main/java/com/wordnik/swagger/codegen/config/python/PythonLibCodeGen.java diff --git a/bin/generate-python-lib.sh b/bin/generate-python-lib.sh new file mode 100755 index 00000000000..06fb8071a3b --- /dev/null +++ b/bin/generate-python-lib.sh @@ -0,0 +1,23 @@ +#!/bin/bash +if [ $# -ne 4 ] +then + echo "Error in $0 - Invalid Argument Count" + echo "Syntax: $0 location_of_service api_key package_name library_root" + exit +fi + +echo "" > classpath.txt +for file in `ls lib`; + do echo -n 'lib/' >> classpath.txt; + echo -n $file >> classpath.txt; + echo -n ':' >> classpath.txt; +done +for file in `ls build`; + do echo -n 'build/' >> classpath.txt; + echo -n $file >> classpath.txt; + echo -n ':' >> classpath.txt; +done + +export CLASSPATH=$(cat classpath.txt):conf/python/templates +export JAVA_OPTS="${JAVA_OPTS} -Dproperty=Xmx2g" +java $WORDNIK_OPTS $JAVA_CONFIG_OPTIONS $JAVA_OPTS -cp $CLASSPATH com.wordnik.swagger.codegen.config.python.PythonLibCodeGen "$@" \ No newline at end of file diff --git a/conf/python/structure/APIClient.py b/conf/python/structure/APIClient.py new file mode 100644 index 00000000000..2f17b12660f --- /dev/null +++ b/conf/python/structure/APIClient.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +"""Wordnik.com's Swagger generic API client. This client handles the client- +server communication, and is invariant across implementations. Specifics of +the methods and models for each application are generated from the Swagger +templates.""" + +import sys +import os +import re +import urllib +import urllib2 +import httplib +import json + +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../') +import model + + +class APIClient: + """Generic API client for Swagger client library builds""" + + def __init__(self, apiKey=None, apiServer=None): + if apiKey == None: + raise Exception('You must pass an apiKey when instantiating the ' + 'APIClient') + self.apiKey = apiKey + self.apiServer = apiServer + + def callAPI(self, resourcePath, method, queryParams, postData, + headerParams=None): + + url = self.apiServer + resourcePath + headers = {} + if headerParams: + for param, value in headerParams.iteritems(): + headers[param] = value + + headers['Content-type'] = 'application/json' + headers['api_key'] = self.apiKey + + data = None + if method == 'GET': + if queryParams: + # Need to remove None values, these should not be sent + sentQueryParams = {} + for param, value in queryParams.iteritems(): + if value != None: + sentQueryParams[param] = value + url = url + '?' + urllib.urlencode(sentQueryParams) + request = urllib2.Request(url=url, headers=headers) + elif method in ['POST', 'PUT', 'DELETE']: + data = postData + if data: + if type(postData) not in [str, int, float, bool]: + data = json.dumps(postData.__dict__) + request = urllib2.Request(url=url, headers=headers, data=data) + if method in ['PUT', 'DELETE']: + # Monkey patch alert! Urllib2 doesn't really do PUT / DELETE + request.get_method = lambda: method + + else: + raise Exception('Method ' + method + ' is not recognized.') + + # Make the request + response = urllib2.urlopen(request).read() + + try: + data = json.loads(response) + except ValueError: # PUT requests don't return anything + data = None + + return data + + def serialize(self, obj): + """ + Args: + obj -- data object to be serialized + Returns: + string -- json serialization of object + """ + return json.dumps(obj) + + def deserialize(self, obj, objClass): + """Derialize a JSON string into an object. + + Args: + obj -- string or object to be deserialized + objClass -- class literal for deserialzied object, or string + of class name + Returns: + object -- deserialized object""" + + # Have to accept objClass as string or actual type. Type could be a + # native Python type, or one of the model classes. + if type(objClass) == str: + try: + objClass = eval(objClass) + except NameError: # not a native type, must be model class + objClass = eval('model.' + objClass + '.' + objClass) + + if objClass in [str, int, float, bool]: + return objClass(obj) + + instance = objClass() + + for attr, attrType in instance.swaggerTypes.iteritems(): + if attr in obj: + value = obj[attr] + if attrType in ['str', 'int', 'float', 'bool']: + attrType = eval(attrType) + try: + value = attrType(value) + except UnicodeEncodeError: + value = unicode(value) + setattr(instance, attr, value) + elif 'list<' in attrType: + match = re.match('list<(.*)>', attrType) + subClass = match.group(1) + subValues = [] + + for subValue in value: + subValues.append(self.deserialize(subValue, subClass)) + setattr(instance, attr, subValues) + else: + setattr(instance, attr, self.deserialize(value, + objClass)) + + return instance diff --git a/conf/python/structure/__init__.py b/conf/python/structure/__init__.py new file mode 100644 index 00000000000..eb47a8860ce --- /dev/null +++ b/conf/python/structure/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +"""Load all of the modules in the models directory.""" +import os + +for module in os.listdir(os.path.dirname(__file__)): + if module != '__init__.py' and module[-3:] == '.py': + __import__(module[:-3], locals(), globals()) diff --git a/conf/python/templates/EnumObject.st b/conf/python/templates/EnumObject.st new file mode 100644 index 00000000000..708ee12eb76 --- /dev/null +++ b/conf/python/templates/EnumObject.st @@ -0,0 +1,25 @@ +#!/usr/bin/env python +""" +Copyright 2011 Wordnik, 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. +""" + +class $className$: + """ + $enum.description$ + NOTE: This class is auto generated by the swagger code generator program. + Do not edit the class manually. + """ + def __init__(): + $values: { value | self.$value.name$ = $value.value$};separator=";\n"$ diff --git a/conf/python/templates/ModelObject.st b/conf/python/templates/ModelObject.st new file mode 100644 index 00000000000..cc37f2db16b --- /dev/null +++ b/conf/python/templates/ModelObject.st @@ -0,0 +1,37 @@ +#!/usr/bin/env python +""" +Copyright 2011 Wordnik, 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. +""" + +class $className$: + """ + $model.description$ + + NOTE: This class is auto generated by the swagger code generator program. + Do not edit the class manually. + """ + + def __init__(self): + self.swaggerTypes = { + $fields: { field |'$field.fieldDefinition.name$': '$field.fieldDefinition.returnType$'};separator=",\n"$ + } + + +$fields:{ field | + + # $field.description$ + self.$field.fieldDefinition.name$ = None # $field.fieldDefinition.returnType$ + +}$ diff --git a/conf/python/templates/ResourceObject.st b/conf/python/templates/ResourceObject.st new file mode 100644 index 00000000000..ab6e17c2ff6 --- /dev/null +++ b/conf/python/templates/ResourceObject.st @@ -0,0 +1,115 @@ +#!/usr/bin/env python +""" +$resource$.py +Copyright 2011 Wordnik, 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. + +NOTE: This class is auto generated by the swagger code generator program. Do not edit the class manually. +""" +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../') +import model + +class $resource$(object): + + def __init__(self, apiClient): + self.apiClient = apiClient + +$methods:{ method | + def $method.name$(self, $method.arguments: { argument | $if(argument.required)$$argument.name$, $endif$}$$method.arguments: { argument | $if(! argument.required)$$argument.name$=None, $endif$}$): + """$method.title$ + $if(method.description)$ + $method.description$ + $endif$ + Args: + $method.arguments:{ argument |$argument.name$ -- $argument.description$ + $if(argument.allowedValues)$ + Allowed values are - $argument.allowedValues$ + $endif$}$ + $if(!method.responseVoid)$ + Return: + $method.returnValue$ -- an instance of $method.returnClassName$ + $endif$""" + + # Parse inputs + resourcePath = '$method.resourcePath$' + resourcePath = resourcePath.replace('{format}', 'json') + method = '$method.methodType$' + + queryParams = {} + headerParams = {} +$if(method.authToken)$ + if not authToken: + raise Exception('missing authToken') + headerParams['auth_token'] = authToken +$endif$ + +$if(!method.inputModel)$ +$method.queryParameters:{ argument | + queryParams['$argument.name$'] = $argument.name$ +}$ + +$method.pathParameters:{ argument | + if $argument.name$ != None: + resourcePath = resourcePath.replace('{$argument.name$}', $argument.name$) + +}$ +$endif$ +$if(method.inputModel)$ +$method.queryParameters:{ argument | + if $argument.inputModelClassArgument$ != None and $argument.inputModelClassArgument$.$argument.name$ != None: + queryParams['$argument.name$'] = $argument.inputModelClassArgument$.$argument.name$ + +}$ +$method.pathParameters:{ argument | + if $argument.inputModelClassArgument$ != None and $argument.inputModelClassArgument$.$argument.name$ != None: + resourcePath = resourcePath.replace('{$argument.name$}', $argument.inputModelClassArgument$.$argument.name$) +}$ +$endif$ + + # Make the API Call +$if(method.postObject)$ + response = self.apiClient.callAPI(resourcePath, method, queryParams, + postData, headerParams) +$endif$ + +$if(!method.postObject)$ + response = self.apiClient.callAPI(resourcePath, method, queryParams, + None, headerParams) +$endif$ + +$if(!method.responseVoid)$ + if not response: + return None + + $if(!method.returnValueList)$ + # Create output objects if the response has more than one object + responseObject = self.apiClient.deserialize(response, + model.$method.returnClassName$.$method.returnClassName$) + return responseObject + $endif$ + + $if(method.returnValueList)$ + responseObjects = [] + for responseObject in response: + responseObjects.append(self.apiClient.deserialize(responseObject, + model.$method.returnClassName$.$method.returnClassName$)) + return responseObjects + $endif$ +$endif$ + +}$ + diff --git a/conf/python/templates/VersionChecker.st b/conf/python/templates/VersionChecker.st new file mode 100644 index 00000000000..1041722e48c --- /dev/null +++ b/conf/python/templates/VersionChecker.st @@ -0,0 +1,36 @@ + \ No newline at end of file diff --git a/src/main/java/com/wordnik/swagger/codegen/config/python/PythonDataTypeMappingProvider.java b/src/main/java/com/wordnik/swagger/codegen/config/python/PythonDataTypeMappingProvider.java new file mode 100644 index 00000000000..21f99514ec7 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/codegen/config/python/PythonDataTypeMappingProvider.java @@ -0,0 +1,228 @@ +/** + * Copyright 2011 Wordnik, 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. + */ + +package com.wordnik.swagger.codegen.config.python; + +import com.wordnik.swagger.codegen.config.DataTypeMappingProvider; +import com.wordnik.swagger.codegen.config.NamingPolicyProvider; +import com.wordnik.swagger.codegen.config.common.CamelCaseNamingPolicyProvider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * User: ramesh + * Date: 5/31/11 + * Time: 7:03 AM + */ +public class PythonDataTypeMappingProvider implements DataTypeMappingProvider { + + public static Map primitiveValueMap = new HashMap(); + static{ + primitiveValueMap.put("string", "str"); + primitiveValueMap.put("String", "str"); + primitiveValueMap.put("int", "int"); + primitiveValueMap.put("integer", "int"); + primitiveValueMap.put("Integer", "int"); + primitiveValueMap.put("boolean", "bool"); + primitiveValueMap.put("Boolean", "bool"); + primitiveValueMap.put("long", "int"); + primitiveValueMap.put("Long", "int"); + primitiveValueMap.put("float", "float"); + primitiveValueMap.put("Float", "float"); + primitiveValueMap.put("Date", "str"); + primitiveValueMap.put("date", "str"); + primitiveValueMap.put("Double", "float"); + primitiveValueMap.put("double", "float"); + } + + public static Map primitiveObjectMap = new HashMap(); + static{ + primitiveObjectMap.put("string", "str"); + primitiveObjectMap.put("String", "str"); + primitiveObjectMap.put("java.lang.String", "str"); + primitiveObjectMap.put("int", "int"); + primitiveObjectMap.put("integer", "int"); + primitiveObjectMap.put("Integer", "int"); + primitiveObjectMap.put("java.lang.Integer", "int"); + primitiveObjectMap.put("bool", "bool"); + primitiveObjectMap.put("boolean", "bool"); + primitiveObjectMap.put("Boolean", "bool"); + primitiveObjectMap.put("java.lang.Boolean", "bool"); + primitiveObjectMap.put("long", "int"); + primitiveObjectMap.put("Long", "int"); + primitiveObjectMap.put("java.lang.Long", "int"); + primitiveObjectMap.put("float", "float"); + primitiveObjectMap.put("Float", "float"); + primitiveObjectMap.put("double", "float"); + primitiveObjectMap.put("Double", "float"); + primitiveObjectMap.put("java.lang.Float", "float"); + primitiveObjectMap.put("Date", "str"); + primitiveObjectMap.put("date", "str"); + primitiveObjectMap.put("java.util.Date", "str"); + } + + private NamingPolicyProvider nameGenerator = new CamelCaseNamingPolicyProvider(); + + public boolean isPrimitiveType(String type) { + if(primitiveObjectMap.containsKey(type)){ + return true; + } + return false; + } + + public String getListReturnTypeSignature(String typeClass) { + if (isPrimitiveType(typeClass)) { + return "list<"+typeClass+">"; + } else { + return "list<"+nameGenerator.applyClassNamingPolicy(typeClass)+">"; + } + } + + public String getMapReturnTypeSignature(String typeClass) { + return "dict<"+nameGenerator.applyClassNamingPolicy(typeClass)+">"; + } + + public String getSetReturnTypeSignature(String typeClass) { + return "set<"+nameGenerator.applyClassNamingPolicy(typeClass)+">"; + } + + public String generateListInitialization(String typeClass) { + return " list()"; + } + + public String generateMapInitialization(String typeClass) { + return " dict()"; + } + + public String generateSetInitialization(String typeClass) { + return " set()"; + } + + public List getListIncludes() { + List imports = new ArrayList(); + return imports; + } + + public List getMapIncludes() { + List imports = new ArrayList(); + return imports; + } + + public List getSetIncludes() { + List imports = new ArrayList(); + return imports; } + + + public List getDateIncludes() { + List imports = new ArrayList(); + return imports; + } + + /** + * Gets the short name of the class the class. + * Input can be MAP, LIST or regular string. In case of map or list the class name will be class name + * that map or list is returning. + * @param type + * @return + */ + public String getGenericType(String type) { + String classShortName = ""; + if(type.startsWith("List[")){ + classShortName = type.substring(5, type.length()-1); + classShortName = getClassType(classShortName, true); + }else if (type.startsWith("Map[")) { + classShortName = type.substring(4, type.length()-1); + classShortName = getClassType(classShortName, true); + }else if (type.startsWith("Set[")) { + classShortName = type.substring(4, type.length()-1); + classShortName = getClassType(classShortName, true); + }else if (type.equalsIgnoreCase("ok")) { + classShortName = "void"; + }else{ + classShortName = getClassType(type, true); + } + return classShortName; + } + + /** + * Returns the syntax for defintion of an object of type and name + * + * @param argumentType + * @param argumentName + * @return + */ + public String getArgumentDefinition(String argumentType, String argumentName) { + return argumentType + " " + argumentName; + } + + /** + * Gets the class of the expected return value for a type string. Examples of type Strings are int, User, List[User] + * If the type string is a collection type like a map or list the string value returned would be the class + * that map or list is returning. + * + * @param type + * @return + */ + public String getClassType(String type, boolean primitiveObject) { + if(type.equalsIgnoreCase("void")|| type.equalsIgnoreCase("ok")){ + return "void"; + } + String classShortName = ""; + if(type.startsWith("List[")){ + classShortName = type.substring(5, type.length()-1); + classShortName = "list<"+ getClassName(classShortName, true)+">"; + }else if (type.startsWith("Map[")) { + classShortName = type.substring(4, type.length()-1); + classShortName = "dict<"+ getClassName(classShortName, true) +">"; + }else if (type.startsWith("Set[")) { + classShortName = type.substring(4, type.length()-1); + classShortName = "set<"+ getClassName(classShortName, true) +">"; + }else{ + classShortName = getClassName(type, true); + } + return classShortName; + } + + + /** + * If the data type is primitive and it is expecting object structure then return primitive objects + * else return primitive types + * @param type + * @param primitiveObject -- indicates if the object is primitive or not + * @return + */ + private String getClassName(String type, boolean primitiveObject) { + if(isPrimitiveType(type)){ + if(primitiveObject){ + return primitiveObjectMap.get(type); + }else{ + return primitiveValueMap.get(type); + } + }else{ + + return nameGenerator.applyClassNamingPolicy(type); + } + } + + @Override + public String generateVariableInitialization(String typeClass) { + return ""; + } + +} diff --git a/src/main/java/com/wordnik/swagger/codegen/config/python/PythonLibCodeGen.java b/src/main/java/com/wordnik/swagger/codegen/config/python/PythonLibCodeGen.java new file mode 100644 index 00000000000..3f35a71c25b --- /dev/null +++ b/src/main/java/com/wordnik/swagger/codegen/config/python/PythonLibCodeGen.java @@ -0,0 +1,103 @@ +/** + * Copyright 2011 Wordnik, 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. + */ + +package com.wordnik.swagger.codegen.config.python; + +import com.wordnik.swagger.codegen.LibraryCodeGenerator; +import com.wordnik.swagger.codegen.config.LanguageConfiguration; +import com.wordnik.swagger.codegen.config.common.CamelCaseNamingPolicyProvider; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; +import com.wordnik.swagger.codegen.util.FileUtil; + +import java.io.File; + +/** + * User: russ + * Date: 9/1/11 + * Time: 11:00 PM + */ +public class PythonLibCodeGen extends LibraryCodeGenerator { + + public static void main(String[] args) { + if(args.length < 1){ + throw new CodeGenerationException("Invalid number of arguments passed: No command line argument was passed to the program for config json"); + } + if(args.length == 1) { + String configPath = args[0]; + PythonLibCodeGen codeGenerator = new PythonLibCodeGen(configPath); + codeGenerator.generateCode(); + } + if(args.length == 4) { + String apiServerURL = args[0]; + if(!apiServerURL.endsWith("/")){ + apiServerURL = apiServerURL + "/"; + } + String apiKey = args[1]; + String packageName = args[2]; + String libraryHome = args[3]; + if(libraryHome.endsWith("/")){ + libraryHome = libraryHome.substring(0, libraryHome.length()-1); + } + String modelPackageName = packageName+".model"; + String apiPackageName = packageName+".api"; + String classOutputDir = libraryHome + packageName.replace(".","/"); + PythonLibCodeGen codeGenerator = new PythonLibCodeGen(apiServerURL, apiKey, modelPackageName, + apiPackageName, classOutputDir, libraryHome); + codeGenerator.generateCode(); + } + + } + + public PythonLibCodeGen(String apiServerURL, String apiKey, String modelPackageName, String apiPackageName, + String classOutputDir, String libraryHome){ + super(apiServerURL, apiKey, modelPackageName, apiPackageName, classOutputDir, libraryHome); + this.setDataTypeMappingProvider(new PythonDataTypeMappingProvider()); + this.setNameGenerator(new CamelCaseNamingPolicyProvider()); + } + + public PythonLibCodeGen(String configPath){ + super(configPath); + this.setDataTypeMappingProvider(new PythonDataTypeMappingProvider()); + this.setNameGenerator(new CamelCaseNamingPolicyProvider()); + } + + @Override + protected LanguageConfiguration initializeLangConfig(LanguageConfiguration PythonConfiguration) { + PythonConfiguration.setClassFileExtension(".py"); + PythonConfiguration.setTemplateLocation("conf/python/templates"); + PythonConfiguration.setStructureLocation("conf/python/structure"); + PythonConfiguration.setExceptionPackageName("com.wordnik.swagger.exception"); + PythonConfiguration.setAnnotationPackageName("com.wordnik.swagger.annotations"); + + //create ouput directories + FileUtil.createOutputDirectories(PythonConfiguration.getModelClassLocation(), PythonConfiguration.getClassFileExtension()); + FileUtil.createOutputDirectories(PythonConfiguration.getResourceClassLocation(), PythonConfiguration.getClassFileExtension()); + FileUtil.clearFolder(PythonConfiguration.getModelClassLocation()); + FileUtil.clearFolder(PythonConfiguration.getResourceClassLocation()); + FileUtil.copyDirectory(new File(PythonConfiguration.getStructureLocation()), new File(PythonConfiguration.getResourceClassLocation())); + File initFile = new File(PythonConfiguration.getResourceClassLocation() + "__init__.py"); + File newInitFile = new File(PythonConfiguration.getModelClassLocation() + "__init__.py"); + initFile.renameTo(newInitFile); + // try { + // initFile.createNewFile(); + // } catch (java.io.IOException e) { + // e.printStackTrace(); + // throw new CodeGenerationException("Creating model/__init__.py failed"); + // } + return PythonConfiguration; + } + +}