All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fi.linuxbox.upcloud.core.Resource.groovy Maven / Gradle / Ivy

Go to download

Groovy UpCloud core provides a way to talk to the UpCloud API and a representation of the Resources

There is a newer version: 0.0.7
Show newest version
package fi.linuxbox.upcloud.core

import groovy.transform.PackageScope
import org.slf4j.*

/**
 * Model for a resource in UpCloud, e.g. server, storage device, or an IP address.
 *
 * 

* Together with the {@link API} class, this class is the most core of the Groovy UpCloud library. You probably don't * use this directly, but you could. This class allows for dynamic creation of UpCloud resource representations and * their properties. *

* *

* For example, at the time of this writing, the UpCloud account API returns credits and * username properties, but no email property. Not saying that it should, but it could be * useful. Now, if a future revision of the API adds that property, it would be immediately available in the * Account resource of this library, even without an update. *

*
 *     HTTP/1.1 200 OK
 *     Content-Type: application/json; charset=UTF-8
 *
 *     {
 *         "account": {
 *             "credits": 1000,
 *             "username": "foo_bar",
 *             "email": "[email protected]"
 *         }
 *     }
 * 
*
 *     upcloud.account({ response ->
 *         assert response.account.email == "[email protected]"
 *     })
 * 
* *

* Similarly, if a future revision of the UpCloud API would add a completely new property to, say, a server, represented * as a new kind of object, then it would be visible in the responses generated by this library. For example: *

*
 *     HTTP/1.1 200 OK
 *     Content-Type: application/json; charset=UTF-8
 *
 *     {
 *         "server": {
 *             ...
 *             "docker_images": {
 *                 "docker_image": [
 *                     {
 *                         "image": "ubuntu:latest",
 *                         "state": "running"
 *                     },
 *                     {
 *                         "image": "nginx:latest",
 *                         "state": "running"
 *                     }
 *                 ]
 *             },
 *             ...
 *         }
 *     }
 * 
* *

* Above, the docker_images property is an imaginary one, but because the server returned it, it would be * available in the application callback as follows: *

* *

 *     server.load({ response ->
 *       assert response.server instanceof fi.linuxbox.upcloud.resource.Server
 *       // Above is old news.
 *       // Now for the fun part:
 *       assert response.server.dockerImages instanceof List
 *       assert response.server.dockerImages.every { it instanceof fi.linuxbox.upcloud.resource.DockerImage }
 *       assert response.server.dockerImages[0].state == "running"
 *     })
 * 
* *

* In reality, you wouldn't be able to write that code exactly as shown, because the DockerImage class is * not available at compile time, i.e. your compiler would complain about the reference. But at runtime the class is * as real as any other resource class. *

* *

* THE FOLLOWING IS JUST UNFINISHED NOTES *

* *

* Representation is always a Map with String keys. The value can be *

* *
    *
  • A list wrapper: a map with one key whose value is a list
  • *
  • A resource: a map that is not a list wrapper
  • *
  • A simple value: anything except a map
  • *
* *

* JSON data types: *

* *
    *
  • object: either a list wrapper or a resource
  • *
  • array, number, string, boolean, null: a simple value
  • *
*/ class Resource { private final Logger log = LoggerFactory.getLogger(Resource) private static final GroovyClassLoader gcl = new GroovyClassLoader(Resource.classLoader) private static final String resourcePackageName = "fi.linuxbox.upcloud.resource" final API API final META META /** * Designated, and only, constructor. * * @param kwargs.API The API instance. This is used by the resource specific API wrappers, not directly by this class. * @param kwargs.META The META instance. This is received from the HTTP implementation. * @param kwargs.repr The Map intermediary representation from the JSON implementations. */ Resource(final Map kwargs = [ :]) { // the second arg (register=false) makes this metaClass instance scoped metaClass = new ExpandoMetaClass(this.class, false, true) metaClass.initialize() API = kwargs.API META = kwargs.META final Map map = kwargs.remove('repr') as Map map?.each { final String key, final Object value -> if (value instanceof Map) { if (isListWrapper(value)) { // value ----v // 'objects': [ 'object': [ [:], ... ] // 'objects': [ 'object': [ string, ... ] final String type_name = value.keySet()[0] // the only key final String className = className(type_name) final String propertyName = propertyName(key) // e.g. // [ // prices: [ // zone: [ // [:], // ... // ] // ] // ] // becomes // this.prices = [ Zone(), ... ] final List list = (List) value[type_name]; this.metaClass."$propertyName" = list.collect { element -> if (element instanceof Map) { final Class clazz = loadResourceClass(className) element = clazz.metaClass.invokeConstructor([repr: element] + kwargs) } element } //__setter_getter(this, attr) } else { // Map but not list wrapper // 'object': [key1: *, key2: *, ...] final String className = className(key) final String propertyName = propertyName(key) final Class clazz = loadResourceClass(className) this.metaClass."$propertyName" = clazz.metaClass.invokeConstructor(kwargs + [repr: value]) //__setter_getter(this, attr) } } else { // not Map // simple attribute final String propertyName = propertyName(key) this.metaClass."$propertyName" = value //__setter_getter(this, attr) } } } /** * Returns properties of this resource. * *

* This method skips GroovyObject properties (every Groovy object has a 'class' property), meta resource properties * (every Resource has 'META' and 'API' properties), and properties whose value is null. *

* * @return Properties of this resource. */ private List> resourceProperties() { this.properties.grep { final Map.Entry property -> if (property.value == null) null else if (property.key == 'class' && property.value instanceof Class) null else if (property.key =~ /^[A-Z]+$/) null else property } } /** * Converts this resource to a Map representation for JSON generation. * *

* This is used as resource as Map and meant to be invoked only from the API class, just before the resource * is put into the queue of to-be-sent requests. *

* *

* Instead of using direct object-to-JSON mapping, this intermediary representation is used because *

*
    *
  • * This allows for a quick snapshot of the resource. Generating the JSON could happen in a background thread. *
  • *
  • * This allows the JSON generator to be stupid. It doesn't have to deal with conversion between Java and * JSON style of property names. Also, it doesn't have to know how to filter the properties, or how to * introspect for them in the first place. *
  • *
  • * This allows the JSON to be changed to Protobuf or XML more easily. Or the JSON implementation to change * mode easily. This is because the requirements for those modules is very low. *
  • *
* * @param clazz Map. Nothing else supported. * @return Representation of this resource. */ Object asType(Class clazz) { resourceProperties().grep { final Map.Entry property -> if (property.value instanceof List && property.value.isEmpty()) null else property }.inject([:]) { final Map map, final Map.Entry property -> final String property_name = type_name(property.key) map[property_name] = property.value.with { if (it instanceof List) { final Object firstElement = it[0] if (firstElement instanceof Resource) { final String type_name = type_name(firstElement.class.simpleName) // this.prices = [ Zone(), ... ] // map[prices] = [ zone: [ [:], ... ] ] // this.timezones = [ "asd", "dsa", ... ] // map[timezones] = [ timezone: [ "asd", "dsa", ... ] ] // the parens for the key force the GString to evaluate to String [ (type_name): it.collect { it as Map }] } else if (firstElement instanceof String) { // HACK: only places I know of where we need this: // * server creation: [ssh_keys: [ssh_key: [key1, key2]]] // * tag creation: [servers: [server: [uuid1, uuid2]]] // * tag update: [servers: [server: [uuid1, uuid2]]] // so naive unpluralization of the property_name (ssh_keys or servers) // will do for those. final String type_name = property_name.substring(0, property_name.size() - 1) [ (type_name): it.collect { it as String }] } else { throw new UnsupportedOperationException("Only lists of Resources or Strings is currently supported!!!") } } else if (it instanceof Resource) { // this.error = Error() it as Map } else { // this.id = "fi-hel1" it } } map } } /** * Returns this resource wrapped in another resource. * *

* Many of the UpCloud APIs require a wrapping JSON object to be sent into the resources, and this method is used * in those places. *

* * @return A wrapped resource whose sole property is this resource. */ def wrapper() { final String propertyName = propertyName(this.class) new Resource(API: API, META: META)."$propertyName"(this) } /** * Returns a projection of this resource. * *

* Projection is a copy of this resource with some of the properties removed. *

* *

* Some of the UpCloud APIs specifically disallow some of the resource properties from being sent in the requests, * and this method is used in those API calls. *

* * @param properties A list of property names to exclude. * @return A copy of this resource with specified properties removed. */ def proj(final List properties) { resourceProperties().grep { final Map.Entry property -> if (property.key in properties) property else null }.inject ((Resource)this.metaClass.invokeConstructor(API: API, META: META)) { final Resource resource, final Map.Entry property -> resource."${property.key}"(property.value) } } /** * MOP method that allows resource properties to be set in a fluent fashion. * *

* Allows for code like server.name('my server').description('My server') to set two properties for * the server resource. *

* * @param name Name of the property. * @param args Value of the property (must be a single element argument array). * @return The resource for chaining. */ def methodMissing(final String name, final def args) { if (args?.length == 1) this."$name" = args[0] this } /** * MOP method that allows resource properties to be missing. * *

* Allows for code like if (server.name) ... without actually defining Java Bean property for the name. *

* * @param name Name of the property. * @return Value of the property or null. */ def propertyMissing(final String name) { // Help the trait proxies see all the properties (we used to return null here, but the proxies didn't work). this.properties[name] } /** * MOP method that allows resource properties to be set. * *

* Allows for code like server.name = "my server" without actually defining Java Bean property for the * name. *

* * @param name Name of the property. * @param arg Value of the property. * @return Value of the property. */ def propertyMissing(final String name, final def arg) { this.metaClass."$name" = arg } /** * Converts Class name to a Java style property name. * * @param clazz Simple class name, i.e. name without the package. * @return Java style property name. */ @PackageScope static String propertyName(final Class clazz) { clazz.simpleName.replaceAll(/([A-Z])([A-Z]+)/, { it[1] + it[2].toLowerCase() }) // RESOURCE -> Resource .replaceFirst(/^([A-Z])/, { it[0].toLowerCase() }) // Server -> server } /** * Converts a JSON property name to a Java style property name. * *

* For example, 'storage_device' becomes 'storageDevice'. *

* * @param type_name JSON style property name. * @return Java style property name. */ @PackageScope static String propertyName(final String type_name) { type_name.replaceAll(/_([a-z])/, { it[1].toUpperCase() }) // type_name -> typeName } /** * Converts a JSON property name to a class name. * *

* For example, 'storage_device' becomes 'StorageDevice'. *

* * @param type_name JSON style property name. * @return Simple class name, i.e. name without the package. */ @PackageScope static String className(final String type_name) { type_name.replaceAll(/(?:^|_)([a-z])/, { it[1].toUpperCase() }) // type_name -> TypeName } /** * Converts a class name to a JSON property name. * *

* For example, 'StorageDevice' becomes 'storage_device'. *

* * @param className Simple name of a class, i.e. name without the package. * @return JSON style property name. */ @PackageScope static String type_name(final String className) { className.replaceAll(/([A-Z])([A-Z]+)/, { it[1] + it[2].toLowerCase() }) // RESOURCE -> Resource .replaceFirst(/^([A-Z])/, { it[0].toLowerCase() }) // Server -> server .replaceAll(/([A-Z])/, { '_' + it[0].toLowerCase() }) // storageDevice -> storage_device } /** * Returns a Class corresponding to the named resource. * *

* For example, loadResourceClass('Server') would return the Class for * fi.linuxbox.upcloud.resource.Server class. *

* * @param resourceClassName Simple name of the resource class, i.e. name without the package. * @return Class for the named resource. */ private Class loadResourceClass(final String resourceClassName) { Class clazz = null try { clazz = gcl.loadClass("${resourcePackageName}.$resourceClassName") log.debug("Loaded resource $resourceClassName") } catch (final ClassNotFoundException ignored) { log.info("Generating resource $resourceClassName") clazz = gcl.parseClass(""" package ${resourcePackageName} import ${Resource.class.name} class $resourceClassName extends ${Resource.class.simpleName} { $resourceClassName(final Map kwargs = [:]) { super(kwargs) } } """) } clazz } /** * Returns true if the value is a list wrapper. * *

* A list wrapper is a Map that has only one key and the value corresponding to that key is a list. This is a * convention in the UpCloud API. *

* * @param value A map which to check. * @return true if value is a list wrapper, false otherwise. */ private static boolean isListWrapper(final Map value) { final Set keys = value.keySet() keys.size() == 1 && value[keys[0]] instanceof List } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy