├── .gitignore ├── README.adoc ├── SequenceGeneratorGrailsPlugin.groovy ├── application.properties ├── grails-app ├── conf │ ├── BuildConfig.groovy │ ├── Config.groovy │ ├── DataSource.groovy │ └── UrlMappings.groovy ├── controllers │ └── grails │ │ └── plugins │ │ └── sequence │ │ └── SequenceGeneratorController.groovy ├── domain │ └── grails │ │ └── plugins │ │ └── sequence │ │ ├── SequenceDefinition.groovy │ │ ├── SequenceNumber.groovy │ │ └── SequenceTestEntity.groovy ├── i18n │ └── sequence-generator.properties ├── services │ └── grails │ │ └── plugins │ │ └── sequence │ │ └── SequenceGeneratorService.groovy └── views │ ├── error.gsp │ └── sequenceGenerator │ └── index.gsp ├── scripts ├── _Install.groovy ├── _Uninstall.groovy └── _Upgrade.groovy ├── src ├── groovy │ └── grails │ │ └── plugins │ │ └── sequence │ │ ├── DefaultSequenceGenerator.groovy │ │ └── SequenceHandle.groovy └── java │ └── grails │ └── plugins │ └── sequence │ ├── BeforeValidateInjection.java │ ├── Sequence.java │ ├── SequenceASTTransformation.java │ ├── SequenceEntity.java │ ├── SequenceGenerator.java │ └── SequenceStatus.java ├── test └── integration │ └── grails │ └── plugins │ └── sequence │ ├── DefaultGeneratorTests.groovy │ └── SequenceServiceTests.groovy └── web-app └── WEB-INF ├── applicationContext.xml ├── sitemesh.xml └── tld ├── c.tld ├── fmt.tld ├── grails.tld └── spring.tld /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Intellij IDEA files 2 | .idea 3 | *.iml 4 | 5 | # Ignore build artifacts 6 | target 7 | 8 | # Ignore packaged plugins 9 | *.zip 10 | *.sha1 11 | 12 | stacktrace.log 13 | plugin.xml 14 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Grails Sequence Generator Plugin 2 | Goran Ehrsson 3 | Version 1.1 4 | :description: Official documentation for the Grails Sequence Generator Plugin 5 | :keywords: groovy, grails, database, sequences 6 | :icons: font 7 | :imagesdir: ./images 8 | :source-highlighter: prettify 9 | 10 | The sequence generator plugin provides a simple way to add sequence counters 11 | to Grails applications. You can control the starting number, the format and 12 | you can have different sequence counters based on application logic. 13 | 14 | == Examples 15 | 16 | [source,groovy] 17 | ---- 18 | sequenceGeneratorService.initSequence('WebOrder', null, null, 100, 'WEB-%04d') 19 | 20 | assert sequenceGeneratorService.nextNumber('WebOrder') == 'WEB-0100' 21 | assert sequenceGeneratorService.nextNumber('WebOrder') == 'WEB-0101' 22 | assert sequenceGeneratorService.nextNumber('WebOrder') == 'WEB-0102' 23 | 24 | assert sequenceGeneratorService.nextNumberLong('WebOrder') == 103 25 | assert sequenceGeneratorService.nextNumberLong('WebOrder') == 104 26 | ---- 27 | 28 | The SequenceGeneratorService implementation is very efficient and can provide 29 | sequential numbers to concurrent threads without problems. 30 | 31 | The default implementation persist sequences to the database to survive server restarts. 32 | 33 | Because it's common to have sequence properties on domain classes (customer number, order number, etc) 34 | there is an annotation that does all the plumbing for you. 35 | Domain classes annotated with `grails.plugins.sequence.SequenceEntity` 36 | will get a `number` property added at compile that will be initialized with 37 | a unique sequence number when the domain instance is saved to database. 38 | 39 | == Configuration 40 | 41 | The sequence generator works without any configuration. 42 | Sensible defaults are used if no configuration is found. 43 | You can customise the sequences with the following parameters in Config.groovy: 44 | 45 | === Number format 46 | 47 | `sequence.(name).format` (default %d) 48 | 49 | Format to use for sequence numbers. The name is the name of the sequence (simple name of the domain class). 50 | The number is formatted with `String#format(String, Object...)`. 51 | 52 | sequence.Customer.format = "%05d" 53 | 54 | === Starting number 55 | 56 | `sequence.(name).start` (default 1) 57 | 58 | The starting number when a sequence is first initialized. The name is the name of the sequence (simple name of the domain class). 59 | 60 | sequence.Customer.start = 1001 61 | 62 | === Flush interval 63 | 64 | `sequence.flushInterval` (default 60) 65 | 66 | This configuration parameter is only available when using the `DefaultSequenceGenerator`. 67 | It specifies the number of seconds to wait before flushing in-memory sequence counters to the database. 68 | 69 | sequence.flushInterval = 30 70 | 71 | == Annotation @SequenceEntity 72 | 73 | If you have a sequence property on a domain class, for example a customer number property, you could add code 74 | in beforeValidate() or beforeInsert() that assigns a sequence number with `sequenceGeneratorService.nextNumber(this.class)`. 75 | But the `grails.plugins.sequence.SequenceEntity` annotation makes this much easier. It does all the plumbing for you. 76 | 77 | [source,groovy] 78 | .Customer.groovy 79 | ---- 80 | @SequenceEntity 81 | class Customer { 82 | ... 83 | } 84 | ---- 85 | 86 | An AST Transformation adds a `String` property called *number* to the domain class at compile time. 87 | The property will by default have `maxSize:10`, `unique:true`, and `blank:false` constraints. 88 | But you can override this in the annotation. 89 | 90 | [source,groovy] 91 | .CustomerOrder.groovy 92 | ---- 93 | @SequenceEntity(property = "orderNumber", maxSize = 20, blank = false, unique = "true") <1> 94 | class CustomerOrder { 95 | ... 96 | } 97 | ---- 98 | <1> Note that the `unique` attribute is a `String`, not a `boolean`. 99 | 100 | The AST Transformation will also add code in `beforeValidate()` that sets the `number` property if it is not already set. 101 | 102 | So the only thing you really have to do is to annotate your domain class with `@SequenceEntity` and the number 103 | property will be set to a new unique number before the domain instance is saved to the database. 104 | 105 | [NOTE] 106 | ==== 107 | Maybe you ask: "Why not use database sequences?" 108 | 109 | Well, a database sequence use numbers only and is very efficient but not so flexible. 110 | This plugin is more flexible and lets you use String properties and prefix/suffix the number with characters. 111 | You can use sub-sequences to generate different numbers depending on application logic. 112 | Maybe domain instances of one category should use another sequence that the default. 113 | This plugin also let you change the sequence number programatically. 114 | For example you could reset the sequence to start with YYYY0001 on the first of January every year. 115 | ==== 116 | 117 | == SequenceGeneratorService 118 | 119 | With `SequenceGeneratorService` you can interact with sequences. The following methods are available: 120 | 121 | `Sequence initSequence(String name, String group, Long tenant, Long start, String format)` 122 | 123 | Create a new sequence counter and initialize it with a starting number (default 1). 124 | 125 | [options="header",cols="1,4"] 126 | |=== 127 | | Parameter | Description 128 | | name | Name of sequence 129 | | group (optional) | If you need multiple sequences for the same domain class based on some application logic you can use groups to create sub-sequences 130 | | tenant (optional) | Tenant ID in a multi-tenant environment 131 | | start (optional) | The sequence will start at this number 132 | | format (optional) | The number format returned by `nextNumber()` uses `String#format(String, Object...)` 133 | |=== 134 | 135 | `Sequence initSequence(Class clazz, String group, Long tenant, Long start, String format)` 136 | 137 | Same as above but takes a domain class instead of sequence name. `Class#getSimpleName()` will be used as sequence name. 138 | 139 | `String nextNumber(String name, String group, Long tenant)` 140 | 141 | Returns the next number in the specified sequence. The number is formatted with the sequence's defined format. 142 | 143 | [options="header",cols="1,4"] 144 | |=== 145 | | Parameter | Description 146 | | name | Name of sequence 147 | | group (optional) | Optional sub-sequence if multiple sequence counters exists for the same name / domain class 148 | | tenant (optional) | Tenant ID in a multi-tenant environment 149 | |=== 150 | 151 | `String nextNumber(Class clazz, String group, Long tenant)` 152 | 153 | Same as above but takes a domain class instead of sequence name. Class#getSimpleName() will be used as sequence name. 154 | 155 | `Long nextNumberLong(String name, String group, Long tenant)` 156 | 157 | If you don't need formatted numbers and just want a number sequence you can use `nextNumberLong()`. 158 | It works the same way as `nextNumber()` but returns a `Long` instead of a formatted `String`. 159 | 160 | `boolean setNextNumber(Long currentNumber, Long newNumber, String name, String group, Long tenant)` 161 | 162 | Sets the next number for a sequence counter. 163 | To avoid concurrency issues you must specify both the current number and the number you want to change to. 164 | If current number is not equal to the specified current number the new number will not be set. 165 | True is returned if the sequence number was updated. 166 | 167 | [options="header",cols="1,4"] 168 | |=== 169 | | Parameter | Description 170 | | currentNumber | The caller's view of what the current number is 171 | | newNumber | The number to set. The next call to `nextNumber()` will get this number 172 | | name | Name of sequence to set number for 173 | | group (optional) | Optional sub-sequence if multiple sequence counters exists for the same name / domain class 174 | | tenant (optional) | Tenant ID in a multi-tenant environment 175 | |=== 176 | 177 | `Iterable statistics(Long tenant)` 178 | 179 | [options="header",cols="1,4"] 180 | |=== 181 | | Parameter | Description 182 | | tenant (optional) | Tenant ID in a multi-tenant environment 183 | |=== 184 | 185 | Return statistics for all sequences defined in the application for a given tenant. 186 | 187 | == REST Controller 188 | 189 | `SequenceGeneratorController` provides two methods that accepts JSON requests to interact with sequences. 190 | 191 | WARNING: Make sure you protect this controller with appropriate access control! 192 | 193 | `list(String name, String group)` 194 | 195 | Returns a list of sequences in JSON format. See `SequenceGeneratorService#getStatistics()` 196 | 197 | `update(String name, String group, Long current, Long next)` 198 | 199 | Accepts POST requests that updates the next number for a sequence. See `SequenceGeneratorService#setNextNumber()` 200 | 201 | == JMX 202 | 203 | You can check sequence statistics from a JMX client using the registered JMX bean `:name=SequenceGeneratorService,type=services`. 204 | 205 | == Changes 206 | 207 | 1.2:: Added 'group' property to the SequenceStatus class. This makes it possible to retrieve group name when parsing statistics. 208 | Also added an admin UI for setting next available sequence number (controller: 'sequenceGenerator', action: 'index') 209 | 1.1:: Renamed SequenceNumber.number column to `sequence_number` because `number` is a reserved word in Oracle DB. 210 | 211 | [TIP] 212 | ==== 213 | When upgrading from 1.0 to 1.1 the *number* property was renamed to *sequence_number*. 214 | The database migration plugin does not handle column renaming so well. 215 | It generates dropColumn and addColumn statements, resulting in data-loss. 216 | The following script is a modified version for MySQL: 217 | 218 | [source,groovy] 219 | ---- 220 | changeSet(author: "nobody", id: "1417018030553-1") { 221 | renameColumn(tableName: 'sequence_number', oldColumnName: 'number', newColumnName: 'sequence_number', columnDataType: 'bigint') 222 | } 223 | ---- 224 | ==== 225 | 226 | 1.0:: First public release 227 | 228 | == Known Issues 229 | 230 | * The current implementation (DefaultSequenceGenerator) keep sequences in memory for performance reasons and therefore it cannot be used in clustered environments. 231 | The (experimental) *sequence-generator-rest* and *sequence-generator-redis* are designed to work in clustered environments, see link below. 232 | 233 | == Road Map 234 | 235 | * Implement a second sequence generator that communicates with an external micro service. 236 | (maybe built with Spring Boot and Redis). This would add clustering support that the current in-memory implementation `DefaultSequenceGenerator` lacks. 237 | ** *Work In Progress:* See https://github.com/goeh/grails-sequence-generator-rest[sequence-generator-rest] for an example of an external sequence generator service. 238 | ** *Work In Progress:* See https://github.com/goeh/grails-sequence-generator-redis[sequence-generator-redis] a sequence generator backed by Redis. 239 | 240 | == License 241 | 242 | This plugin is licensed with http://www.apache.org/licenses/LICENSE-2.0.html[Apache License version 2.0] 243 | 244 | == Miscellaneous 245 | 246 | The http://gr8crm.github.io[GR8 CRM ecosystem] uses the sequence-generator plugin to generate customer, order and invoice numbers. 247 | 248 | The https://grails.org/plugin/url-shortener[URL Shortener Plugin] can use this sequence-generator plugin to generate unique numbers. -------------------------------------------------------------------------------- /SequenceGeneratorGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | import grails.plugins.sequence.SequenceEntity 19 | import org.slf4j.Logger 20 | import org.slf4j.LoggerFactory 21 | import org.springframework.context.ApplicationContext 22 | import org.springframework.jmx.export.MBeanExporter 23 | import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource 24 | import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler 25 | import org.springframework.jmx.support.MBeanServerFactoryBean 26 | 27 | import javax.management.MBeanServer 28 | import javax.management.ObjectName 29 | import java.lang.management.ManagementFactory 30 | 31 | class SequenceGeneratorGrailsPlugin { 32 | def version = "1.2.1" 33 | def grailsVersion = "2.2 > *" 34 | def dependsOn = [:] 35 | def loadAfter = ['domainClass', 'services'] 36 | def pluginExcludes = [ 37 | "grails-app/views/error.gsp", 38 | "grails-app/domain/grails/plugins/sequence/SequenceTestEntity.groovy" 39 | ] 40 | def title = "Sequence Number Generator" 41 | def author = "Goran Ehrsson" 42 | def authorEmail = "goran@technipelago.se" 43 | def description = ''' 44 | A Grails service that generate sequence numbers from different sequences, formats, etc. 45 | You can control the starting number, the format and you can have different sequences based on application logic. 46 | The method getNextSequenceNumber() is injected into all domain classes annotated with @SequenceEntity. 47 | It returns the next number for the sequence defined for the domain class. 48 | ''' 49 | def documentation = "https://github.com/goeh/grails-sequence-generator" 50 | def license = "APACHE" 51 | def organization = [name: "Technipelago AB", url: "http://www.technipelago.se/"] 52 | def issueManagement = [system: "github", url: "https://github.com/goeh/grails-sequence-generator/issues"] 53 | def scm = [url: "https://github.com/goeh/grails-sequence-generator"] 54 | 55 | private Logger LOG = LoggerFactory.getLogger('grails.plugins.sequence.SequenceGeneratorGrailsPlugin') 56 | 57 | private final String JMX_OBJECT_NAME = ':name=SequenceGeneratorService,type=services' 58 | 59 | def doWithSpring = { 60 | 61 | sequenceGenerator(grails.plugins.sequence.DefaultSequenceGenerator) { bean -> 62 | bean.autowire = 'byName' 63 | } 64 | 65 | //create/find the mbean server 66 | mbeanServer(MBeanServerFactoryBean) { 67 | locateExistingServerIfPossible = true 68 | } 69 | //use annotations for attributes/operations 70 | jmxAttributeSource(AnnotationJmxAttributeSource) 71 | assembler(MetadataMBeanInfoAssembler) { 72 | attributeSource = jmxAttributeSource 73 | } 74 | //create an exporter that uses annotations 75 | annotationExporter(MBeanExporter) { 76 | server = mbeanServer 77 | assembler = assembler 78 | beans = [:] 79 | } 80 | } 81 | 82 | def doWithApplicationContext = { applicationContext -> 83 | def config = application.config 84 | for (c in application.domainClasses) { 85 | if (c.clazz.getAnnotation(SequenceEntity)) { 86 | addDomainMethods(applicationContext, config, c.metaClass) 87 | } 88 | } 89 | registerJMX(applicationContext, application.metadata.getApplicationName() + JMX_OBJECT_NAME) 90 | } 91 | 92 | def onShutdown = { event -> 93 | unregisterJMX(event.ctx, application.metadata.getApplicationName() + JMX_OBJECT_NAME) 94 | } 95 | 96 | private void addDomainMethods(ctx, config, MetaClass mc) { 97 | def service = ctx.getBean('sequenceGeneratorService') 98 | mc.getNextSequenceNumber = { group = null -> 99 | def name = delegate.class.simpleName 100 | def tenant = delegate.hasProperty('tenantId') ? delegate.tenantId : null 101 | def nbr 102 | delegate.class.withNewSession { 103 | nbr = service.nextNumber(name, group, tenant) 104 | } 105 | return nbr 106 | } 107 | } 108 | 109 | private void registerJMX(ApplicationContext ctx, String jmxObjectName) { 110 | try { 111 | MBeanServer mbs = ManagementFactory.getPlatformMBeanServer() 112 | if (mbs.isRegistered(new ObjectName(jmxObjectName))) { 113 | LOG.info("MBean $jmxObjectName already registered") 114 | } else { 115 | MBeanExporter annotationExporter = ctx.getBean("annotationExporter") 116 | annotationExporter.beans."$jmxObjectName" = ctx.getBean("sequenceGeneratorService") 117 | annotationExporter.registerBeans() 118 | } 119 | } catch (Exception e) { 120 | LOG.warn("Failed to register $jmxObjectName", e) 121 | } 122 | } 123 | 124 | private void unregisterJMX(ApplicationContext ctx, String jmxObjectName) { 125 | try { 126 | MBeanServer mbs = ManagementFactory.getPlatformMBeanServer() 127 | if (mbs.isRegistered(new ObjectName(jmxObjectName))) { 128 | mbs.unregisterMBean(new ObjectName(jmxObjectName)) 129 | } else { 130 | LOG.info("MBean $jmxObjectName not registered") 131 | } 132 | } catch (Exception e) { 133 | LOG.warn("Failed to unregister $jmxObjectName", e) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /application.properties: -------------------------------------------------------------------------------- 1 | #Grails Metadata file 2 | #Sun Aug 19 16:18:43 CEST 2012 3 | app.grails.version=2.4.5 4 | app.name=sequence-generator 5 | -------------------------------------------------------------------------------- /grails-app/conf/BuildConfig.groovy: -------------------------------------------------------------------------------- 1 | grails.project.work.dir = "target" 2 | grails.project.class.dir = "target/classes" 3 | grails.project.test.class.dir = "target/test-classes" 4 | grails.project.test.reports.dir = "target/test-reports" 5 | grails.project.target.level = 1.6 6 | 7 | grails.project.dependency.resolver = "maven" 8 | grails.project.dependency.resolution = { 9 | inherits("global") {} 10 | log "warn" 11 | repositories { 12 | grailsCentral() 13 | mavenCentral() 14 | } 15 | dependencies { 16 | } 17 | plugins { 18 | build(":release:3.0.1", 19 | ":rest-client-builder:1.0.3") { 20 | export = false 21 | } 22 | test(":hibernate4:4.3.6.1") { 23 | export = false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /grails-app/conf/Config.groovy: -------------------------------------------------------------------------------- 1 | // configuration for plugin testing - will not be included in the plugin zip 2 | 3 | log4j = { 4 | // Example of changing the log pattern for the default console 5 | // appender: 6 | // 7 | //appenders { 8 | // console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n') 9 | //} 10 | 11 | error 'org.codehaus.groovy.grails.web.servlet', // controllers 12 | 'org.codehaus.groovy.grails.web.pages', // GSP 13 | 'org.codehaus.groovy.grails.web.sitemesh', // layouts 14 | 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping 15 | 'org.codehaus.groovy.grails.web.mapping', // URL mapping 16 | 'org.codehaus.groovy.grails.commons', // core / classloading 17 | 'org.codehaus.groovy.grails.plugins', // plugins 18 | 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration 19 | 'org.springframework', 20 | 'org.hibernate', 21 | 'net.sf.ehcache.hibernate' 22 | 23 | warn 'org.mortbay.log' 24 | 25 | debug 'grails.app.services.grails.plugins.sequence' 26 | } 27 | 28 | sequence.flushInterval = 5 29 | grails.views.default.codec="none" // none, html, base64 30 | grails.views.gsp.encoding="UTF-8" 31 | -------------------------------------------------------------------------------- /grails-app/conf/DataSource.groovy: -------------------------------------------------------------------------------- 1 | dataSource { 2 | pooled = true 3 | jmxExport = true 4 | driverClassName = "org.h2.Driver" 5 | username = "sa" 6 | password = "" 7 | } 8 | hibernate { 9 | cache.use_second_level_cache = true 10 | cache.use_query_cache = false 11 | // cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory' // Hibernate 3 12 | cache.region.factory_class = 'org.hibernate.cache.ehcache.EhCacheRegionFactory' // Hibernate 4 13 | singleSession = true // configure OSIV singleSession mode 14 | flush.mode = 'manual' // OSIV session flush mode outside of transactional context 15 | } 16 | // environment specific settings 17 | environments { 18 | development { 19 | dataSource { 20 | dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', '' 21 | url = "jdbc:h2:mem:devDb;MVCC=TRUE" 22 | } 23 | } 24 | test { 25 | dataSource { 26 | dbCreate = "update" 27 | url = "jdbc:h2:mem:testDb;MVCC=TRUE" 28 | } 29 | } 30 | production { 31 | dataSource { 32 | dbCreate = "update" 33 | url = "jdbc:h2:prodDb;MVCC=TRUE" 34 | pooled = true 35 | properties { 36 | maxActive = -1 37 | minEvictableIdleTimeMillis=1800000 38 | timeBetweenEvictionRunsMillis=1800000 39 | numTestsPerEvictionRun=3 40 | testOnBorrow=true 41 | testWhileIdle=true 42 | testOnReturn=true 43 | validationQuery="SELECT 1" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /grails-app/conf/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | class UrlMappings { 2 | 3 | static mappings = { 4 | "/$controller/$action?/$id?"{ 5 | constraints { 6 | // apply constraints here 7 | } 8 | } 9 | 10 | "/"(view:"/index") 11 | "500"(view:'/error') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /grails-app/controllers/grails/plugins/sequence/SequenceGeneratorController.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | import grails.converters.JSON 21 | 22 | /** 23 | * Admin actions for the sequence generator. 24 | */ 25 | class SequenceGeneratorController { 26 | 27 | def sequenceGeneratorService 28 | def currentTenant 29 | 30 | static allowedMethods = [update: 'POST'] 31 | 32 | def index(String name, String group, Long current, Long next) { 33 | def tenant = currentTenant?.get()?.longValue() 34 | if (request.post) { 35 | if (sequenceGeneratorService.setNextNumber(current, next, name, group, tenant)) { 36 | flash.message = message(code: 'sequenceGenerator.update.message', default: "Sequence updated", args: [name, group, current, next]) 37 | } else { 38 | flash.message = message(code: 'sequenceGenerator.update.error', default: "Sequence could not be updated", args: [name, group, current, next]) 39 | } 40 | } 41 | [result: sequenceGeneratorService.statistics(tenant)] 42 | } 43 | 44 | def list(String name, String group) { 45 | def tenant = currentTenant?.get()?.longValue() 46 | def result = sequenceGeneratorService.statistics(tenant) 47 | if (name) { 48 | result = result.findAll { it.name == name } 49 | } 50 | if (group) { 51 | result = result.findAll { it.group == group } 52 | } 53 | render result as JSON 54 | } 55 | 56 | def update(String name, String group, Long current, Long next) { 57 | def tenant = currentTenant?.get()?.longValue() 58 | def rval = [:] 59 | if (sequenceGeneratorService.setNextNumber(current, next, name, group, tenant)) { 60 | rval.success = true 61 | rval.message = 'Sequence number updated' 62 | } else { 63 | rval.success = false 64 | rval.message = 'Current number was not current, please try again.' 65 | } 66 | render rval as JSON 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /grails-app/domain/grails/plugins/sequence/SequenceDefinition.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | /** 21 | * Domain class for persisting sequence definitions. 22 | */ 23 | class SequenceDefinition { 24 | 25 | Long tenantId 26 | String name 27 | String format 28 | static hasMany = [numbers:SequenceNumber] 29 | static constraints = { 30 | tenantId(nullable:true) 31 | name(unique:'tenantId', blank:false, maxSize:100) 32 | format(nullable:true, maxSize:100) 33 | } 34 | static mapping = { 35 | cache true 36 | } 37 | 38 | @Override 39 | String toString() { 40 | tenantId != null ? "${tenantId}.${name}".toString() : name 41 | } 42 | 43 | @Override 44 | int hashCode() { 45 | int hash = 17 46 | if(id != null) hash = hash * 17 + id * 17 47 | if(version != null) hash = hash * 17 + version * 17 48 | if(tenantId != null) hash = hash * 17 + tenantId * 17 49 | if(name != null) hash = hash * 17 + name.hashCode() 50 | return hash 51 | } 52 | 53 | @Override 54 | boolean equals(other) { 55 | if(this.is(other)) { 56 | return true 57 | } 58 | if(other == null) { 59 | return false 60 | } 61 | if (!(other.instanceOf(SequenceDefinition))) { 62 | return false 63 | } 64 | if(!(this.id != null ? this.id.equals(other.id) : other.id == null)) { 65 | return false 66 | } 67 | if(!(this.version != null ? this.version.equals(other.version) : other.version == null)) { 68 | return false 69 | } 70 | if(!(this.tenantId != null ? this.tenantId.equals(other.tenantId) : other.tenantId == null)) { 71 | return false 72 | } 73 | if(!(this.name != null ? this.name.equals(other.name) : other.name == null)) { 74 | return false 75 | } 76 | return true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /grails-app/domain/grails/plugins/sequence/SequenceNumber.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | package grails.plugins.sequence 18 | 19 | /** 20 | * Domain class that holds the next available sequence number for a specific sequence definition. 21 | */ 22 | class SequenceNumber { 23 | 24 | String group 25 | Long number 26 | 27 | static belongsTo = [definition:SequenceDefinition] 28 | static constraints = { 29 | group(nullable:true, blank:false, maxSize:40, unique:'definition') 30 | } 31 | static mapping = { 32 | group column:'sequence_group' 33 | number column:'sequence_number' 34 | } 35 | 36 | SequenceHandle toHandle() { 37 | new SequenceHandle(number, definition.format) 38 | } 39 | 40 | @Override 41 | String toString() { 42 | "$number" 43 | } 44 | 45 | @Override 46 | int hashCode() { 47 | int hash = 17 48 | if(id != null) hash = hash * 17 + id * 17 49 | if(version != null) hash = hash * 17 + version * 17 50 | if(group != null) hash = hash * 17 + group.hashCode() 51 | if(number != null) hash = hash * 17 + number.hashCode() 52 | return hash 53 | } 54 | 55 | @Override 56 | boolean equals(other) { 57 | if(this.is(other)) { 58 | return true 59 | } 60 | if(other == null) { 61 | return false 62 | } 63 | if (!(other.instanceOf(SequenceNumber))) { 64 | return false 65 | } 66 | if(!(this.id != null ? this.id.equals(other.id) : other.id == null)) { 67 | return false 68 | } 69 | if(!(this.version != null ? this.version.equals(other.version) : other.version == null)) { 70 | return false 71 | } 72 | if(!(this.group != null ? this.group.equals(other.group) : other.group == null)) { 73 | return false 74 | } 75 | if(!(this.number != null ? this.number.equals(other.number) : other.number == null)) { 76 | return false 77 | } 78 | return true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /grails-app/domain/grails/plugins/sequence/SequenceTestEntity.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | /** 21 | * Only used by integration test. 22 | */ 23 | @SequenceEntity 24 | class SequenceTestEntity { 25 | String name 26 | 27 | String toString() { 28 | "#$number $name" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /grails-app/i18n/sequence-generator.properties: -------------------------------------------------------------------------------- 1 | sequenceGenerator.index.title=Sequence Administration 2 | sequenceGenerator.next.label=Next Number 3 | sequenceGenerator.next.help=Next available number in this sequence 4 | sequenceGenerator.update.label=Update 5 | sequenceGenerator.update.message=Sequence updated 6 | sequenceGenerator.update.error=Failed to update sequence! -------------------------------------------------------------------------------- /grails-app/services/grails/plugins/sequence/SequenceGeneratorService.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | import groovy.transform.CompileStatic 21 | import org.springframework.jmx.export.annotation.ManagedAttribute 22 | import org.springframework.jmx.export.annotation.ManagedResource 23 | 24 | /** 25 | * A service that provide sequence counters (for customer numbers, invoice numbers, etc) 26 | * This service has two primary methods: nextNumber() and nextNumberLong(). 27 | */ 28 | @ManagedResource(description = "Grails Sequence Generator") 29 | class SequenceGeneratorService { 30 | 31 | static transactional = false 32 | 33 | SequenceGenerator sequenceGenerator 34 | 35 | @CompileStatic 36 | SequenceStatus initSequence(Class clazz, String group = null, Long tenant = null, Long start = null, String format = null) { 37 | initSequence(clazz.simpleName, group, tenant, start, format) 38 | } 39 | 40 | @CompileStatic 41 | SequenceStatus initSequence(String name, String group = null, Long tenant = null, Long start = null, String format = null) { 42 | sequenceGenerator.create(tenant ?: 0L, name, group, format, start != null ? start : 1L) 43 | } 44 | 45 | @CompileStatic 46 | String nextNumber(Class clazz, String group = null, Long tenant = null) { 47 | nextNumber(clazz.simpleName, group, tenant) 48 | } 49 | 50 | @CompileStatic 51 | String nextNumber(String name, String group = null, Long tenant = null) { 52 | initSequence(name, group, tenant) 53 | sequenceGenerator.nextNumber(tenant ?: 0L, name, group) 54 | } 55 | 56 | @CompileStatic 57 | Long nextNumberLong(String name, String group = null, Long tenant = null) { 58 | initSequence(name, group, tenant) 59 | sequenceGenerator.nextNumberLong(tenant ?: 0L, name, group).longValue() 60 | } 61 | 62 | @CompileStatic 63 | boolean setNextNumber(Long currentNumber, Long newNumber, String name, String group = null, Long tenant = null) { 64 | if (currentNumber != newNumber) { 65 | if(sequenceGenerator.update(tenant ?: 0L, name, group, null, currentNumber, newNumber)) { 66 | log.debug "Sequence [$name] in tenant [$tenant] changed from [$currentNumber] to [$newNumber]" 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | 73 | SequenceStatus status(String name, String group = null, Long tenant = null) { 74 | sequenceGenerator.status(tenant ?: 0L, name, group) 75 | } 76 | 77 | Iterable statistics(Long tenant = null) { 78 | sequenceGenerator.getStatistics(tenant ?: 0L) 79 | } 80 | 81 | @ManagedAttribute(description = "Sequence generator statistics") 82 | String getStatistics() { 83 | statistics().collect { "${it.name}=${it.number}" }.join(', ') 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /grails-app/views/error.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Grails Runtime Exception 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /grails-app/views/sequenceGenerator/index.gsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | 6 | <g:message code="sequenceGenerator.index.title" default="Sequence Administration"/> 7 | 8 | 9 | 10 | 11 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |

24 | ${message(code: n.name[0].toLowerCase() + n.name.substring(1) + '.label', default: n.name)} 25 | 26 | (${n.group}) 27 | 28 |

29 | 30 |
31 | 33 | 34 |
35 | 37 | 41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /scripts/_Install.groovy: -------------------------------------------------------------------------------- 1 | // 2 | // This script is executed by Grails after plugin was installed to project. 3 | // This script is a Gant script so you can use all special variables provided 4 | // by Gant (such as 'baseDir' which points on project base dir). You can 5 | // use 'ant' to access a global instance of AntBuilder 6 | // 7 | // For example you can create directory under project tree: 8 | // 9 | // ant.mkdir(dir:"${basedir}/grails-app/jobs") 10 | // 11 | -------------------------------------------------------------------------------- /scripts/_Uninstall.groovy: -------------------------------------------------------------------------------- 1 | // 2 | // This script is executed by Grails when the plugin is uninstalled from project. 3 | // Use this script if you intend to do any additional clean-up on uninstall, but 4 | // beware of messing up SVN directories! 5 | // 6 | -------------------------------------------------------------------------------- /scripts/_Upgrade.groovy: -------------------------------------------------------------------------------- 1 | // 2 | // This script is executed by Grails during application upgrade ('grails upgrade' 3 | // command). This script is a Gant script so you can use all special variables 4 | // provided by Gant (such as 'baseDir' which points on project base dir). You can 5 | // use 'ant' to access a global instance of AntBuilder 6 | // 7 | // For example you can create directory under project tree: 8 | // 9 | // ant.mkdir(dir:"${basedir}/grails-app/jobs") 10 | // 11 | -------------------------------------------------------------------------------- /src/groovy/grails/plugins/sequence/DefaultSequenceGenerator.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | import groovy.transform.CompileStatic 21 | import org.codehaus.groovy.grails.commons.GrailsApplication 22 | import org.slf4j.Logger 23 | import org.slf4j.LoggerFactory 24 | import org.springframework.dao.OptimisticLockingFailureException 25 | 26 | import java.util.concurrent.ConcurrentHashMap 27 | 28 | /** 29 | * Created by goran on 2014-06-23. 30 | */ 31 | class DefaultSequenceGenerator implements SequenceGenerator { 32 | 33 | private static 34 | final Map> activeSequences = new ConcurrentHashMap>() 35 | private static final Logger log = LoggerFactory.getLogger(DefaultSequenceGenerator.class) 36 | 37 | GrailsApplication grailsApplication 38 | 39 | private boolean keepGoing 40 | private boolean persisterRunning 41 | private Thread persisterThread 42 | 43 | private void initPersister() { 44 | if (persisterThread == null) { 45 | synchronized (activeSequences) { 46 | if (persisterThread == null) { 47 | def interval = 1000 * (grailsApplication.config.sequence.flushInterval ?: 60) 48 | persisterThread = new Thread("GrailsSequenceGenerator") 49 | persisterThread.start { 50 | persisterRunning = true 51 | keepGoing = true 52 | log.info "Sequence persister thread started with [$interval ms] flush interval" 53 | while (keepGoing) { 54 | try { 55 | Thread.currentThread().sleep(interval) 56 | log.trace("Scheduled flush") 57 | synchronized (activeSequences) { 58 | flush() 59 | } 60 | } catch (InterruptedException e) { 61 | log.info("Sequence flusher thread interrupted") 62 | synchronized (activeSequences) { 63 | flush() 64 | } 65 | } catch (Exception e) { 66 | if (log != null) { 67 | log.error "Failed to flush sequences to database!", e 68 | } else { 69 | e.printStackTrace() 70 | } 71 | } 72 | } 73 | persisterRunning = false 74 | log.info "Sequence persister thread stopped" 75 | } 76 | 77 | Runtime.runtime.addShutdownHook { 78 | terminate() 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | @CompileStatic 86 | private String generateKey(final String name, final String group, final Long tenant) { 87 | final StringBuilder s = new StringBuilder() 88 | if (name != null) { 89 | s.append(name) 90 | } 91 | s.append('/') 92 | if (group != null) { 93 | s.append(group) 94 | } 95 | s.append('/') 96 | if (tenant != null) { 97 | s.append(tenant.toString()) 98 | } 99 | return s.toString() 100 | } 101 | 102 | private SequenceHandle getHandle(String name, String group = null, Long tenant = null) { 103 | def key = generateKey(name, group, tenant) 104 | def h = activeSequences.get(key) 105 | if (h == null) { 106 | def seq = findNumber(name, group, tenant) 107 | if (seq != null) { 108 | h = seq.toHandle() 109 | log.debug "Loaded existing sequence [$key] starting at [${h.number}] with format [${h.format}]" 110 | } else { 111 | def config = grailsApplication.config.sequence 112 | def start = config."$name".initPersister ?: 1L 113 | def format = config."$name".format ?: '%d' 114 | h = createHandle(start, format) 115 | log.debug "Created new sequence [$key] starting at [${h.number}] with format [${h.format}]" 116 | } 117 | synchronized (activeSequences) { 118 | SequenceHandle tmp = activeSequences.get(key) 119 | if (tmp != null) { 120 | h = tmp 121 | } else { 122 | activeSequences.put(key, h) 123 | } 124 | } 125 | initPersister() 126 | } 127 | 128 | return h 129 | } 130 | 131 | @CompileStatic 132 | private SequenceHandle createHandle(final T start, final String format) { 133 | new SequenceHandle(start, format) 134 | } 135 | 136 | @CompileStatic 137 | private void terminate() { 138 | keepGoing = false 139 | Thread t = persisterThread 140 | if (t != null) { 141 | synchronized (t) { 142 | if (persisterThread != null) { 143 | persisterThread = null 144 | t.interrupt() 145 | try { 146 | t.join(5000L) 147 | } catch (InterruptedException e) { 148 | log.error("Error shutting down persister thread", e) 149 | } catch (NullPointerException e) { 150 | log.error("Persister thread was already terminated", e) 151 | } finally { 152 | log.debug "Sequence generator terminated" 153 | if (!activeSequences.isEmpty()) { 154 | log.debug "Active sequences: $activeSequences" 155 | activeSequences.clear() 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | private Collection getDirtySequences() { 164 | activeSequences.findAll { it.value.dirty }.keySet() 165 | } 166 | 167 | private void flush() { 168 | def dirtyKeys = getDirtySequences() 169 | if (dirtyKeys) { 170 | if (log.isDebugEnabled()) { 171 | log.debug("Saving dirty sequences: $dirtyKeys") 172 | } 173 | } else if (log.isTraceEnabled()) { 174 | log.trace("All sequences are clean") 175 | } 176 | 177 | for (String key in dirtyKeys) { 178 | List parts = key.split('/').toList() 179 | String name = parts[0] 180 | String group = parts[1] ?: null 181 | Long tenant = parts[2] ? Long.valueOf(parts[2]) : null 182 | SequenceHandle handle = activeSequences.get(key) 183 | synchronized (handle) { 184 | T n = handle.number 185 | SequenceDefinition.withTransaction { tx -> 186 | SequenceNumber seq = findNumber(name, group, tenant) 187 | if (seq) { 188 | int counter = 10 189 | while (counter--) { 190 | try { 191 | seq = SequenceNumber.lock(seq.id) 192 | counter = 0 193 | } catch (OptimisticLockingFailureException e) { 194 | log.warn "SequenceNumber locked, retrying..." 195 | Thread.currentThread().sleep(25) 196 | } 197 | } 198 | seq.number = n 199 | seq.save(failOnError: true) 200 | } else { 201 | SequenceDefinition d = findDefinition(name, tenant) 202 | if (d) { 203 | d.addToNumbers(group: group, number: n) 204 | } else { 205 | d = new SequenceDefinition(tenantId: tenant, name: name, format: handle.format) 206 | d.addToNumbers(group: group, number: n) 207 | } 208 | d.save(failOnError: true) 209 | } 210 | } 211 | handle.dirty = false 212 | if (log.isTraceEnabled()) { 213 | log.trace "Saved sequence $key = $n" 214 | } 215 | } 216 | } 217 | } 218 | 219 | private SequenceDefinition findDefinition(final String name, final Long tenant) { 220 | SequenceDefinition.createCriteria().get { 221 | eq('name', name) 222 | if (tenant != null) { 223 | eq('tenantId', tenant) 224 | } else { 225 | isNull('tenantId') 226 | } 227 | cache true 228 | } 229 | } 230 | 231 | private SequenceNumber findNumber(final String name, final String group, final Long tenant) { 232 | SequenceNumber.createCriteria().get { 233 | definition { 234 | eq('name', name) 235 | if (tenant != null) { 236 | eq('tenantId', tenant) 237 | } else { 238 | isNull('tenantId') 239 | } 240 | } 241 | if (group != null) { 242 | eq('group', group) 243 | } else { 244 | isNull('group') 245 | } 246 | } 247 | } 248 | 249 | @Override 250 | SequenceStatus create(long tenant, String name, String group, String format, T start) { 251 | def key = generateKey(name, group, tenant) 252 | def h = activeSequences.get(key) 253 | if (h == null) { 254 | synchronized (activeSequences) { 255 | h = activeSequences.get(key) 256 | if (h == null) { 257 | def seq = findNumber(name, group, tenant) 258 | if (seq != null) { 259 | h = seq.toHandle() 260 | log.debug "Loaded existing sequence [$key] starting at [${h.number}] with format [${h.format}]" 261 | } else { 262 | def config = grailsApplication.config.sequence 263 | if (start == null) { 264 | start = config."$name".start ?: (config."$name".initPersister ?: 1L) 265 | } 266 | if (!format) { 267 | format = config."$name".format ?: '%d' 268 | } 269 | h = createHandle(start, format) 270 | log.debug "Created new sequence [$key] starting at [${h.number}] with format [${h.format}]" 271 | } 272 | activeSequences.put(key, h) 273 | } 274 | } 275 | initPersister() 276 | } 277 | new SequenceStatus(name, group, h.getFormat(), h.getNumber()) 278 | } 279 | 280 | /** 281 | * Delete a sequence. 282 | * 283 | * @param tenant tenant ID 284 | * @param name sequence name 285 | * @param group sub-sequence 286 | * @return true if sequence was removed 287 | */ 288 | @Override 289 | boolean delete(long tenant, String name, String group) { 290 | def n = findNumber(name, group, tenant) 291 | if (!n) { 292 | return false 293 | } 294 | def d = n.definition 295 | if (d.numbers.size() == 1) { 296 | d.delete() // There was only one sequence with this name, delete definition (cascades to numbers) 297 | } else { 298 | d.removeFromNumbers(n) 299 | n.delete() // Delete only this sequence/group 300 | } 301 | return true 302 | } 303 | 304 | @CompileStatic 305 | void refresh(long tenant, String name, String group) { 306 | SequenceNumber n = findNumber(name, group, tenant) 307 | if (n) { 308 | SequenceHandle h = getHandle(name, group, tenant) 309 | if (h) { 310 | synchronized (h) { 311 | if (!h.dirty) { 312 | SequenceNumber.withTransaction { 313 | n = (SequenceNumber) SequenceNumber.lock(n.id) 314 | h.setNumber((T) n.number) 315 | } 316 | } 317 | } 318 | } 319 | } 320 | } 321 | 322 | @Override 323 | @CompileStatic 324 | String nextNumber(long tenant, String name, String group) { 325 | getHandle(name, group, tenant).nextFormatted() 326 | } 327 | 328 | @Override 329 | @CompileStatic 330 | T nextNumberLong(long tenant, String name, String group) { 331 | getHandle(name, group, tenant).next() 332 | } 333 | 334 | /** 335 | * This method exists only to make it possible to have the url-shortener plugin co-exist 336 | * with the sequence-generator plugin. Both plugins relies on a bean called sequenceGenerator. 337 | * The SequenceGenerator interface in url-shortener only have one method Long getNextNumber() so 338 | * we imitate that method here and url-shortener can get its sequences from this generator. 339 | * 340 | * @return next sequence number for the url-shortener plugin. 341 | */ 342 | T getNextNumber() { 343 | final ConfigObject config = grailsApplication.config.shortener.sequence 344 | final Long tenant = config.tenant ?: 0L; 345 | final String name = config.name ?: 'UrlShortener' 346 | final String group = config.group ?: null 347 | nextNumberLong(tenant, name, group) 348 | } 349 | 350 | @Override 351 | @CompileStatic 352 | SequenceStatus update(long tenant, String name, String group, String format, T current, T start) { 353 | boolean rval = false 354 | if (format != null) { 355 | def definition = findDefinition(name, tenant) 356 | if (definition != null && definition.format != format) { 357 | definition.format = format 358 | definition.save(flush: true) 359 | rval = true 360 | } 361 | } 362 | if (current != null && start != null) { 363 | SequenceHandle h = getHandle(name, group, tenant) 364 | if (h.number == current) { 365 | synchronized (h) { 366 | if (h.getNumber() == current) { 367 | h.setNumber(start) 368 | rval = true 369 | } 370 | } 371 | } 372 | } 373 | if (rval) { 374 | def handle = getHandle(name, group, tenant) 375 | return new SequenceStatus(name, group, handle.getFormat(), handle.getNumber()) 376 | } 377 | null 378 | } 379 | 380 | @Override 381 | @CompileStatic 382 | SequenceStatus status(long tenant, String name, String group) { 383 | SequenceHandle h = getHandle(name, group, tenant) 384 | new SequenceStatus(name, group, h.getFormat(), h.getNumber()) 385 | } 386 | 387 | @Override 388 | Iterable getStatistics(long tenant) { 389 | SequenceNumber.withTransaction { 390 | synchronized (activeSequences) { 391 | flush() 392 | } 393 | final List numbers = SequenceNumber.createCriteria().list() { 394 | definition { 395 | eq('tenantId', tenant) 396 | order 'name' 397 | } 398 | order 'group' 399 | } 400 | final List result = [] 401 | for (SequenceNumber n in numbers) { 402 | SequenceDefinition d = n.definition 403 | SequenceHandle handle = getHandle(d.name, n.group, d.tenantId) 404 | result << new SequenceStatus(d.name, n.group, d.format, handle.getNumber()) 405 | } 406 | result 407 | } 408 | } 409 | 410 | void sync() { 411 | SequenceNumber.withTransaction { 412 | synchronized (activeSequences) { 413 | flush() 414 | } 415 | } 416 | } 417 | 418 | @CompileStatic 419 | @Override 420 | synchronized void shutdown() { 421 | keepGoing = false 422 | try { 423 | synchronized (activeSequences) { 424 | flush() 425 | } 426 | } catch (Exception e) { 427 | log.error "Failed to save sequence counters!", e 428 | } 429 | terminate() 430 | } 431 | 432 | } 433 | -------------------------------------------------------------------------------- /src/groovy/grails/plugins/sequence/SequenceHandle.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | import java.util.concurrent.atomic.AtomicLong 21 | 22 | /** 23 | * In-memory storage for sequence counters. 24 | */ 25 | class SequenceHandle implements grails.plugins.sequence.Sequence, Serializable { 26 | private final AtomicLong number = new AtomicLong(0L) 27 | String format 28 | boolean dirty 29 | 30 | SequenceHandle(T n) { 31 | this.@number.set(n.longValue()) 32 | dirty = false 33 | } 34 | 35 | SequenceHandle(T n, String fmt) { 36 | this.@number.set(n.longValue()) 37 | format = fmt 38 | dirty = false 39 | } 40 | 41 | @Override 42 | public T getNumber() { 43 | this.@number.get() 44 | } 45 | 46 | public void setNumber(T n) { 47 | dirty = true 48 | this.@number.set(n != null ? n.longValue() : 0L) 49 | } 50 | 51 | @Override 52 | public T next() { 53 | dirty = true 54 | return this.@number.getAndIncrement(); 55 | } 56 | 57 | @Override 58 | public String nextFormatted() { 59 | String.format(format ?: '%s', next()) 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | String.format(format ?: '%s', getNumber()) 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/java/grails/plugins/sequence/BeforeValidateInjection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Rico Krasowski. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | package grails.plugins.sequence; 18 | 19 | import org.codehaus.groovy.ast.ClassHelper; 20 | import org.codehaus.groovy.ast.ClassNode; 21 | import org.codehaus.groovy.ast.MethodNode; 22 | import org.codehaus.groovy.ast.Parameter; 23 | import org.codehaus.groovy.ast.expr.ArgumentListExpression; 24 | import org.codehaus.groovy.ast.expr.BinaryExpression; 25 | import org.codehaus.groovy.ast.expr.BooleanExpression; 26 | import org.codehaus.groovy.ast.expr.MethodCallExpression; 27 | import org.codehaus.groovy.ast.expr.VariableExpression; 28 | import org.codehaus.groovy.ast.stmt.BlockStatement; 29 | import org.codehaus.groovy.ast.stmt.EmptyStatement; 30 | import org.codehaus.groovy.ast.stmt.ExpressionStatement; 31 | import org.codehaus.groovy.ast.stmt.IfStatement; 32 | import org.codehaus.groovy.ast.stmt.ReturnStatement; 33 | import org.codehaus.groovy.ast.stmt.Statement; 34 | import org.codehaus.groovy.grails.compiler.injection.GrailsASTUtils; 35 | import org.codehaus.groovy.syntax.Token; 36 | 37 | import java.lang.reflect.Modifier; 38 | import java.util.List; 39 | 40 | final class BeforeValidateInjection { 41 | 42 | private static final Token ASSIGN_OPERATOR = Token.newSymbol("=", -1, -1); 43 | private static final Token EQUALS_OPERATOR = Token.newSymbol("==", -1, -1); 44 | 45 | public static void generate(ClassNode clazz, String fieldName) { 46 | MethodNode beforeValidateMethod = findMethod(clazz); 47 | if (beforeValidateMethod == null) { 48 | addMethod(clazz, fieldName); 49 | } 50 | else { 51 | injectMethod(beforeValidateMethod, fieldName); 52 | } 53 | } 54 | 55 | private static MethodNode findMethod(ClassNode clazz) { 56 | final List methods = clazz.getMethods("beforeValidate"); 57 | if (methods != null && methods.size() > 0) { 58 | for (MethodNode methodNode : methods) { 59 | if (methodNode.getParameters().length == 0) { 60 | return methodNode; 61 | } 62 | } 63 | } 64 | return null; 65 | } 66 | 67 | private static void addMethod(ClassNode clazz, String fieldName) { 68 | final BlockStatement block = new BlockStatement(); 69 | block.addStatement(createBeforeValidateAst(fieldName)); 70 | block.addStatement(new ReturnStatement(GrailsASTUtils.NULL_EXPRESSION)); 71 | clazz.addMethod(new MethodNode("beforeValidate", Modifier.PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, block)); 72 | } 73 | 74 | private static void injectMethod(MethodNode methodNode, String fieldName) { 75 | final BlockStatement methodBody = (BlockStatement) methodNode.getCode(); 76 | List statements = methodBody.getStatements(); 77 | statements.add(0, createBeforeValidateAst(fieldName)); 78 | } 79 | 80 | /** 81 | * Generates Statement: 82 | * if (fieldName == null) fieldName = this.getNextSequenceNumber(); 83 | */ 84 | private static Statement createBeforeValidateAst(String fieldName) { 85 | return new IfStatement( 86 | /* (fieldName == null) */ 87 | new BooleanExpression(new BinaryExpression( 88 | new VariableExpression(fieldName), 89 | EQUALS_OPERATOR, 90 | GrailsASTUtils.NULL_EXPRESSION) 91 | ), 92 | /* fieldName = this.getNextSequenceNumber(); */ 93 | new ExpressionStatement(new BinaryExpression( 94 | new VariableExpression(fieldName), 95 | ASSIGN_OPERATOR, 96 | new MethodCallExpression(new VariableExpression("this"), "getNextSequenceNumber", ArgumentListExpression.EMPTY_ARGUMENTS) 97 | )), 98 | new EmptyStatement() 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/java/grails/plugins/sequence/Sequence.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence; 19 | 20 | /** 21 | * Created by goran on 2014-06-23. 22 | */ 23 | public interface Sequence { 24 | T getNumber(); 25 | T next(); 26 | String nextFormatted(); 27 | } 28 | -------------------------------------------------------------------------------- /src/java/grails/plugins/sequence/SequenceASTTransformation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence; 19 | 20 | import groovy.lang.ExpandoMetaClass; 21 | import org.codehaus.groovy.ast.*; 22 | import org.codehaus.groovy.ast.expr.*; 23 | import org.codehaus.groovy.ast.stmt.BlockStatement; 24 | import org.codehaus.groovy.ast.stmt.ExpressionStatement; 25 | import org.codehaus.groovy.ast.stmt.Statement; 26 | import org.codehaus.groovy.classgen.VariableScopeVisitor; 27 | import org.codehaus.groovy.control.CompilePhase; 28 | import org.codehaus.groovy.control.SourceUnit; 29 | import org.codehaus.groovy.grails.compiler.injection.GrailsASTUtils; 30 | import org.codehaus.groovy.transform.ASTTransformation; 31 | import org.codehaus.groovy.transform.GroovyASTTransformation; 32 | 33 | import java.lang.reflect.Modifier; 34 | 35 | @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) 36 | public class SequenceASTTransformation implements ASTTransformation { 37 | 38 | //private static final Log LOG = LogFactory.getLog(SequenceASTTransformation.class); 39 | 40 | public void visit(ASTNode[] nodes, SourceUnit sourceUnit) { 41 | 42 | ExpandoMetaClass.disableGlobally(); 43 | 44 | for (ASTNode astNode : nodes) { 45 | if (astNode instanceof ClassNode) { 46 | ClassNode theClass = (ClassNode) astNode; 47 | AnnotationNode sequenceDefinition = GrailsASTUtils.findAnnotation(ClassHelper.make(SequenceEntity.class), theClass.getAnnotations()); 48 | 49 | Expression propertyExpr = sequenceDefinition.getMember("property"); 50 | if(propertyExpr == null) { 51 | propertyExpr = new ConstantExpression("number"); 52 | } 53 | String propertyName = propertyExpr.getText(); 54 | 55 | if (!GrailsASTUtils.hasOrInheritsProperty(theClass, propertyName)) { 56 | System.out.println("Adding sequence field [" + propertyName + "] to class " + theClass.getName()); 57 | 58 | Expression maxSize = sequenceDefinition.getMember("maxSize"); 59 | Expression blank = sequenceDefinition.getMember("blank"); 60 | Expression unique = sequenceDefinition.getMember("unique"); 61 | if(unique != null) { 62 | String uniqueText = unique.getText(); 63 | if("true".equalsIgnoreCase(uniqueText)) { 64 | unique = ConstantExpression.TRUE; 65 | } else if("false".equalsIgnoreCase(uniqueText)) { 66 | unique = ConstantExpression.FALSE; 67 | } else { 68 | unique = new ConstantExpression(uniqueText); 69 | } 70 | } 71 | theClass.addProperty(propertyName, Modifier.PUBLIC, ClassHelper.STRING_TYPE, null, null, null); 72 | Statement numberConstraintExpression = createStringConstraint(propertyExpr, maxSize, blank, unique); 73 | 74 | PropertyNode constraints = theClass.getProperty("constraints"); 75 | if (constraints != null) { 76 | if (constraints.getInitialExpression() instanceof ClosureExpression) { 77 | ClosureExpression ce = (ClosureExpression) constraints.getInitialExpression(); 78 | ((BlockStatement) ce.getCode()).addStatement(numberConstraintExpression); 79 | } else { 80 | System.err.println("Do not know how to add constraints expression to non ClosureExpression " + constraints.getInitialExpression()); 81 | } 82 | } else { 83 | Statement[] constraintsStatement = {numberConstraintExpression}; 84 | BlockStatement closureBlock = new BlockStatement(constraintsStatement, null); 85 | ClosureExpression constraintsClosure = new ClosureExpression(null, closureBlock); 86 | theClass.addProperty("constraints", Modifier.STATIC | Modifier.PUBLIC, ClassHelper.OBJECT_TYPE, constraintsClosure, null, null); 87 | } 88 | } 89 | 90 | BeforeValidateInjection.generate(theClass, propertyName); 91 | 92 | VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(sourceUnit); 93 | scopeVisitor.visitClass(theClass); 94 | } 95 | } 96 | 97 | ExpandoMetaClass.enableGlobally(); 98 | } 99 | 100 | private Statement createStringConstraint(Expression propertyName, Expression maxSize, Expression blank, Expression unique) { 101 | NamedArgumentListExpression nale = new NamedArgumentListExpression(); 102 | if(maxSize != null) { 103 | nale.addMapEntryExpression(new MapEntryExpression(new ConstantExpression("maxSize"), maxSize)); 104 | } 105 | if(blank != null) { 106 | nale.addMapEntryExpression(new MapEntryExpression(new ConstantExpression("blank"), blank)); 107 | } 108 | if(unique != null) { 109 | nale.addMapEntryExpression(new MapEntryExpression(new ConstantExpression("unique"), unique)); 110 | } 111 | 112 | MethodCallExpression mce = new MethodCallExpression(VariableExpression.THIS_EXPRESSION, propertyName, nale); 113 | return new ExpressionStatement(mce); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/java/grails/plugins/sequence/SequenceEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence; 19 | 20 | import org.codehaus.groovy.transform.GroovyASTTransformationClass; 21 | 22 | import java.lang.annotation.ElementType; 23 | import java.lang.annotation.Retention; 24 | import java.lang.annotation.RetentionPolicy; 25 | import java.lang.annotation.Target; 26 | 27 | @Target({ElementType.TYPE}) 28 | @Retention(RetentionPolicy.RUNTIME) 29 | @GroovyASTTransformationClass("grails.plugins.sequence.SequenceASTTransformation") 30 | public @interface SequenceEntity { 31 | String property() default "number"; 32 | int maxSize() default 10; 33 | boolean blank() default false; 34 | String unique() default "true"; 35 | } 36 | -------------------------------------------------------------------------------- /src/java/grails/plugins/sequence/SequenceGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence; 19 | 20 | /** 21 | * Sequence Generator interface. 22 | */ 23 | public interface SequenceGenerator { 24 | /** 25 | * Create a new sequence. 26 | * 27 | * @param tenant tenant ID 28 | * @param name sequence name 29 | * @param group sub-sequence 30 | * @param format number format 31 | * @param start start number 32 | * @return current sequence status 33 | */ 34 | SequenceStatus create(long tenant, String name, String group, String format, T start); 35 | 36 | /** 37 | * Delete a sequence. 38 | * 39 | * @param tenant tenant ID 40 | * @param name sequence name 41 | * @param group sub-sequence 42 | * @return true if sequence was removed 43 | */ 44 | boolean delete(long tenant, String name, String group); 45 | 46 | /** 47 | * Get next unique number formatted. 48 | * 49 | * @param tenant tenant ID 50 | * @param name sequence name 51 | * @param group sub-sequence 52 | * @return formatted number 53 | */ 54 | String nextNumber(long tenant, String name, String group); 55 | 56 | /** 57 | * Get next unique (raw) number. 58 | * 59 | * @param tenant tenant ID 60 | * @param name sequence name 61 | * @param group sub-sequence 62 | * @return number as a long 63 | */ 64 | T nextNumberLong(long tenant, String name, String group); 65 | 66 | /** 67 | * Update sequence. 68 | * 69 | * @param tenant tenant ID 70 | * @param name sequence name 71 | * @param group sub-sequence 72 | * @param format number format 73 | * @param current current number 74 | * @param start new number 75 | * @return sequence status if sequence was updated, null otherwise 76 | */ 77 | SequenceStatus update(long tenant, String name, String group, String format, T current, T start); 78 | 79 | /** 80 | * Current status of a sequence. 81 | * 82 | * @param tenant tenant ID 83 | * @param name sequence name 84 | * @param group sub-sequence 85 | * @return current status 86 | */ 87 | SequenceStatus status(long tenant, String name, String group); 88 | 89 | /** 90 | * Get sequence statistics. 91 | * 92 | * @param tenant tenant ID 93 | * @return statistics for all sequences in the tenant 94 | */ 95 | Iterable getStatistics(long tenant); 96 | 97 | /** 98 | * Shutdown the sequence generator. 99 | * Implementations can close connections, do cleanup, etc. 100 | */ 101 | void shutdown(); 102 | } 103 | -------------------------------------------------------------------------------- /src/java/grails/plugins/sequence/SequenceStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence; 19 | 20 | /** 21 | * A sequence generator snapshot. 22 | */ 23 | public class SequenceStatus { 24 | private final long timestamp; 25 | private final String name; 26 | private String group; 27 | private final String format; 28 | private final long number; 29 | 30 | public SequenceStatus(String name, String group, String format, T number) { 31 | this.name = name; 32 | this.group = group; 33 | this.format = format; 34 | this.number = number != null ? number.longValue() : 0L; 35 | this.timestamp = System.currentTimeMillis(); 36 | } 37 | 38 | public long getTimestamp() { 39 | return timestamp; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public String getGroup() { 47 | return group; 48 | } 49 | 50 | public String getFormat() { 51 | return format; 52 | } 53 | 54 | public long getNumber() { 55 | return number; 56 | } 57 | 58 | String getNumberFormatted() { 59 | return String.format(format != null ? format : "%s", number); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return name + "=" + getNumberFormatted(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/integration/grails/plugins/sequence/DefaultGeneratorTests.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | /** 21 | * Created by goran on 2014-06-24. 22 | */ 23 | class DefaultGeneratorTests extends GroovyTestCase { 24 | 25 | def sequenceGeneratorService 26 | def sequenceGenerator 27 | 28 | void tearDown() { 29 | super.tearDown() 30 | sequenceGenerator.shutdown() 31 | } 32 | 33 | void testSetNumber() { 34 | def name = "Ticket" 35 | def seq = sequenceGeneratorService.initSequence(name, null, null, 121001) 36 | 37 | assertEquals '121001', sequenceGeneratorService.nextNumber(name) 38 | assertEquals '121002', sequenceGeneratorService.nextNumber(name) 39 | assertEquals '121003', sequenceGeneratorService.nextNumber(name) 40 | 41 | assert !sequenceGeneratorService.setNextNumber(121003, 131001, name) 42 | assert sequenceGeneratorService.setNextNumber(121004, 131001, name) 43 | 44 | assertEquals '131001', sequenceGeneratorService.nextNumber(name) 45 | assertEquals '131002', sequenceGeneratorService.nextNumber(name) 46 | assertEquals '131003', sequenceGeneratorService.nextNumber(name) 47 | 48 | sequenceGenerator.shutdown() 49 | 50 | assertEquals '131004', sequenceGeneratorService.nextNumber(name) 51 | assertEquals '131005', sequenceGeneratorService.nextNumber(name) 52 | assertEquals '131006', sequenceGeneratorService.nextNumber(name) 53 | } 54 | 55 | void testThreads() { 56 | def sequenceName = 'ThreadTestDefault' 57 | 58 | final int THREADS = 100 59 | final int NUMBERS = 1000 60 | 61 | sequenceGeneratorService.initSequence(sequenceName, null, null, 1000) 62 | 63 | // Start THREADS threads that grab NUMBERS numbers each. 64 | def slots = new ArrayList(THREADS) 65 | def threads = new ArrayList(THREADS) 66 | for (int i = 0; i < THREADS; i++) { 67 | def arr = slots[i] = new ArrayList(NUMBERS + 1) 68 | def runnable = { 69 | arr << System.currentTimeMillis() 70 | SequenceDefinition.withNewSession { 71 | //println "Thread ${Thread.currentThread().id} started" 72 | NUMBERS.times { 73 | arr << sequenceGeneratorService.nextNumberLong(sequenceName) 74 | Thread.currentThread().sleep(10) 75 | } 76 | arr[0] = System.currentTimeMillis() - arr[0] 77 | if ((Thread.currentThread().id.intValue() % (THREADS / 3).intValue()) == 0) { 78 | sequenceGenerator.refresh(0L, sequenceName, null) 79 | // Be evil and reset all counters from db in the middle of the test. 80 | } 81 | //println "Thread ${Thread.currentThread().id} finished" 82 | } 83 | } 84 | def t = new Thread(runnable, sequenceName + i) 85 | t.priority = Thread.MIN_PRIORITY 86 | threads << t 87 | t.start() 88 | } 89 | threads.each { it.join() } // Wait for all threads to finish. 90 | 91 | long time = 0L 92 | slots.eachWithIndex { arr, i -> 93 | def end = arr.size() - 1 94 | for (int n = 1; n < end; n++) { 95 | int nbr = arr[n] 96 | slots.eachWithIndex { other, l -> 97 | if ((l != i) && other.contains(nbr)) { 98 | println "slot[$i] = ${slots[i][1..-1].join(',')}" 99 | println "slot[$l] = ${slots[l][1..-1].join(',')}" 100 | fail "slot[$l] and slot[$i] both contains $nbr" 101 | } 102 | } 103 | } 104 | time += arr[0] 105 | println "${String.format('%3d', i + 1)} ${slots[i][1..15].join(',')}... (${slots[i][0]} ms)" 106 | } 107 | 108 | println "Average time ${(time / slots.size()).intValue()} ms" 109 | assertEquals 1000 + THREADS * NUMBERS, sequenceGeneratorService.nextNumberLong(sequenceName) 110 | 111 | Thread.sleep(3000) // Wait for persister to finish 112 | 113 | assertEquals 1001 + THREADS * NUMBERS, sequenceGeneratorService.nextNumberLong(sequenceName) 114 | } 115 | 116 | void testShutdown() { 117 | sequenceGeneratorService.initSequence(SequenceTestEntity, null, null, 1008, '%05d') 118 | assertEquals "01008", new SequenceTestEntity().getNextSequenceNumber() 119 | assertEquals "01009", new SequenceTestEntity().getNextSequenceNumber() 120 | assertEquals "01010", new SequenceTestEntity().getNextSequenceNumber() 121 | assertEquals "01011", new SequenceTestEntity().getNextSequenceNumber() 122 | 123 | sequenceGenerator.shutdown() 124 | 125 | assertFalse sequenceGenerator.keepGoing 126 | 127 | assertEquals "01012", new SequenceTestEntity().getNextSequenceNumber() 128 | assertTrue sequenceGenerator.persisterRunning 129 | assertTrue sequenceGenerator.keepGoing 130 | } 131 | 132 | void testRefresh() { 133 | sequenceGeneratorService.initSequence(SequenceTestEntity, null, null, 5001, '%04d') 134 | assertEquals "5001", new SequenceTestEntity().getNextSequenceNumber() 135 | assertEquals "5002", new SequenceTestEntity().getNextSequenceNumber() 136 | assertEquals "5003", new SequenceTestEntity().getNextSequenceNumber() 137 | 138 | sequenceGenerator.sync() 139 | 140 | def n = SequenceNumber.createCriteria().get { 141 | definition { 142 | eq('name', SequenceTestEntity.class.simpleName) 143 | eq('tenantId', 0L) 144 | } 145 | isNull('group') 146 | } 147 | assertNotNull n 148 | 149 | n.number = 2001 150 | n.save(flush: true) 151 | 152 | sequenceGenerator.refresh(0L, SequenceTestEntity.simpleName, null) 153 | 154 | assertEquals "2001", new SequenceTestEntity().getNextSequenceNumber() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/integration/grails/plugins/sequence/SequenceServiceTests.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Goran Ehrsson. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * under the License. 16 | */ 17 | 18 | package grails.plugins.sequence 19 | 20 | import javax.management.MBeanServer 21 | import javax.management.ObjectName 22 | 23 | class SequenceServiceTests extends GroovyTestCase { 24 | 25 | def grailsApplication 26 | def sequenceGeneratorService 27 | def sequenceGenerator 28 | 29 | void tearDown() { 30 | super.tearDown() 31 | sequenceGenerator.shutdown() 32 | } 33 | 34 | void testNoFormat() { 35 | def name = "Company" 36 | sequenceGeneratorService.initSequence(name, null, null, 1) 37 | assertEquals 1L, sequenceGeneratorService.nextNumberLong(name) 38 | assertEquals "2", sequenceGeneratorService.nextNumber(name) 39 | assertEquals 3L, sequenceGeneratorService.nextNumberLong(name) 40 | 41 | Thread.sleep(3000) // Wait for persister to finish 42 | 43 | assertEquals "4", sequenceGeneratorService.nextNumber(name) 44 | } 45 | 46 | void testFormat() { 47 | def name = "Customer" 48 | sequenceGeneratorService.initSequence(name, null, null, 100, 'K-%04d') 49 | 50 | assertEquals 'K-0100', sequenceGeneratorService.nextNumber(name) 51 | assertEquals 'K-0101', sequenceGeneratorService.nextNumber(name) 52 | assertEquals 'K-0102', sequenceGeneratorService.nextNumber(name) 53 | 54 | Thread.sleep(3000) // Wait for persister to finish 55 | 56 | assertEquals 'K-0103', sequenceGeneratorService.nextNumber(name) 57 | 58 | //sequenceGeneratorService.save() 59 | 60 | assertEquals 104, sequenceGeneratorService.status(name, null, null).number 61 | } 62 | 63 | void testSetNumber() { 64 | def name = "Ticket" 65 | sequenceGeneratorService.initSequence(name, null, null, 121001) 66 | 67 | assertEquals '121001', sequenceGeneratorService.nextNumber(name) 68 | assertEquals '121002', sequenceGeneratorService.nextNumber(name) 69 | assertEquals '121003', sequenceGeneratorService.nextNumber(name) 70 | 71 | assert !sequenceGeneratorService.setNextNumber(121003, 131001, name) 72 | assert sequenceGeneratorService.setNextNumber(121004, 131001, name) 73 | 74 | assertEquals '131001', sequenceGeneratorService.nextNumber(name) 75 | assertEquals '131002', sequenceGeneratorService.nextNumber(name) 76 | assertEquals '131003', sequenceGeneratorService.nextNumber(name) 77 | } 78 | 79 | void testThreads() { 80 | def sequenceName = 'ThreadTest' 81 | 82 | final int THREADS = 100 83 | final int NUMBERS = 1000 84 | 85 | sequenceGeneratorService.initSequence(sequenceName, null, null, 1000) 86 | 87 | // Start THREADS threads that grab NUMBERS numbers each. 88 | def slots = new ArrayList(THREADS) 89 | def threads = new ArrayList(THREADS) 90 | for (int i = 0; i < THREADS; i++) { 91 | def arr = slots[i] = new ArrayList(NUMBERS + 1) 92 | def runnable = { 93 | arr << System.currentTimeMillis() 94 | SequenceDefinition.withNewSession { 95 | NUMBERS.times { 96 | arr << sequenceGeneratorService.nextNumberLong(sequenceName) 97 | Thread.currentThread().sleep(10) 98 | } 99 | arr[0] = System.currentTimeMillis() - arr[0] 100 | } 101 | } 102 | def t = new Thread(runnable, sequenceName + i) 103 | t.priority = Thread.MIN_PRIORITY 104 | threads << t 105 | t.start() 106 | } 107 | threads.each { it.join() } // Wait for all threads to finish. 108 | 109 | long time = 0L 110 | slots.eachWithIndex { arr, i -> 111 | def end = arr.size() - 1 112 | for (int n = 1; n < end; n++) { 113 | int nbr = arr[n] 114 | slots.eachWithIndex { other, l -> 115 | if ((l != i) && other.contains(nbr)) { 116 | println "slot[$i] = ${slots[i][1..-1].join(',')}" 117 | println "slot[$l] = ${slots[l][1..-1].join(',')}" 118 | fail "slot[$l] and slot[$i] both contains $nbr" 119 | } 120 | } 121 | } 122 | time += arr[0] 123 | println "${String.format('%3d', i + 1)} ${slots[i][1..15].join(',')}... (${slots[i][0]} ms)" 124 | } 125 | 126 | println "Average time ${(time / slots.size()).intValue()} ms" 127 | assertEquals 1000 + THREADS * NUMBERS, sequenceGeneratorService.nextNumberLong(sequenceName) 128 | 129 | Thread.sleep(3000) // Wait for persister to finish 130 | 131 | assertEquals 1001 + THREADS * NUMBERS, sequenceGeneratorService.nextNumberLong(sequenceName) 132 | } 133 | 134 | void testDomainMethod() { 135 | sequenceGeneratorService.initSequence(SequenceTestEntity, null, null, 1000, '%05d') 136 | assertEquals "01000", new SequenceTestEntity().getNextSequenceNumber() 137 | assertEquals "01001", new SequenceTestEntity().getNextSequenceNumber() 138 | assertEquals "01002", new SequenceTestEntity().getNextSequenceNumber() 139 | assertEquals "01003", new SequenceTestEntity().getNextSequenceNumber() 140 | } 141 | 142 | void testBeforeValidate() { 143 | sequenceGeneratorService.initSequence(SequenceTestEntity, null, null, 1000) 144 | def test = new SequenceTestEntity(name: "TEST") 145 | assert test.respondsTo("beforeValidate") 146 | assert test.number == null 147 | test.beforeValidate() 148 | assert test.number == "1000" 149 | } 150 | 151 | void testStatistics() { 152 | sequenceGeneratorService.initSequence('Foo', null, null, 25) 153 | sequenceGeneratorService.initSequence('Bar', null, null, 50, 'B%d') 154 | 155 | 5.times { 156 | sequenceGeneratorService.nextNumber('Foo') 157 | sequenceGeneratorService.nextNumber('Bar') 158 | } 159 | 160 | def stats = sequenceGeneratorService.statistics() 161 | println "stats=$stats" 162 | def foo = stats.find { it.name == 'Foo' } 163 | assert foo != null 164 | assert foo.format == '%d' 165 | assert foo.number == 30L 166 | def bar = stats.find { it.name == 'Bar' } 167 | assert bar != null 168 | assert bar.name == 'Bar' 169 | assert bar.format == 'B%d' 170 | assert bar.number == 55L 171 | } 172 | 173 | void testClassArgument() { 174 | sequenceGeneratorService.initSequence(SequenceTestEntity, null, null, 1000, '%05d') 175 | assertEquals "01000", sequenceGeneratorService.nextNumber(SequenceTestEntity) 176 | assertEquals "01001", sequenceGeneratorService.nextNumber(SequenceTestEntity) 177 | assertEquals "01002", sequenceGeneratorService.nextNumber(SequenceTestEntity) 178 | } 179 | 180 | void testStartWithZero() { 181 | sequenceGeneratorService.initSequence("Zero", null, null, 0) 182 | assertEquals "0", sequenceGeneratorService.nextNumber("Zero") 183 | } 184 | 185 | void testMultiTenant() { 186 | sequenceGeneratorService.initSequence("TenantTest", null, 0, 100) 187 | sequenceGeneratorService.initSequence("TenantTest", null, 1, 100) 188 | sequenceGeneratorService.initSequence("TenantTest", null, 2, 200) 189 | assertEquals 100, sequenceGeneratorService.nextNumberLong("TenantTest", null, 0) 190 | assertEquals 100, sequenceGeneratorService.nextNumberLong("TenantTest", null, 1) 191 | assertEquals 200, sequenceGeneratorService.nextNumberLong("TenantTest", null, 2) 192 | assertEquals 101, sequenceGeneratorService.nextNumberLong("TenantTest", null, 0) 193 | assertEquals 102, sequenceGeneratorService.nextNumberLong("TenantTest", null, 0) 194 | assertEquals 103, sequenceGeneratorService.nextNumberLong("TenantTest", null, 0) 195 | assertEquals 201, sequenceGeneratorService.nextNumberLong("TenantTest", null, 2) 196 | assertEquals 202, sequenceGeneratorService.nextNumberLong("TenantTest", null, 2) 197 | assertEquals 203, sequenceGeneratorService.nextNumberLong("TenantTest", null, 2) 198 | assertEquals 101, sequenceGeneratorService.nextNumberLong("TenantTest", null, 1) 199 | assertEquals 102, sequenceGeneratorService.nextNumberLong("TenantTest", null, 1) 200 | } 201 | 202 | 203 | private ObjectName getJmxObjectName() { 204 | new ObjectName(grailsApplication.metadata.getApplicationName() + ':name=SequenceGeneratorService,type=services') 205 | } 206 | 207 | void testMBean() { 208 | sequenceGeneratorService.initSequence(SequenceTestEntity, null, null, 1001, '%04d') 209 | assertEquals "1001", new SequenceTestEntity().getNextSequenceNumber() 210 | assertEquals "1002", new SequenceTestEntity().getNextSequenceNumber() 211 | 212 | MBeanServer server = grailsApplication.mainContext.getBean('mbeanServer') 213 | assertTrue server.getAttribute(jmxObjectName, 'Statistics').toString().contains("SequenceTestEntity=1003") 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /web-app/WEB-INF/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Grails application factory bean 9 | 10 | 11 | 12 | 13 | 14 | A bean that manages Grails plugins 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | utf-8 31 | 32 | 33 | -------------------------------------------------------------------------------- /web-app/WEB-INF/sitemesh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web-app/WEB-INF/tld/c.tld: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | JSTL 1.2 core library 9 | JSTL core 10 | 1.2 11 | c 12 | http://java.sun.com/jsp/jstl/core 13 | 14 | 15 | 16 | Provides core validation features for JSTL tags. 17 | 18 | 19 | org.apache.taglibs.standard.tlv.JstlCoreTLV 20 | 21 | 22 | 23 | 24 | 25 | Catches any Throwable that occurs in its body and optionally 26 | exposes it. 27 | 28 | catch 29 | org.apache.taglibs.standard.tag.common.core.CatchTag 30 | JSP 31 | 32 | 33 | Name of the exported scoped variable for the 34 | exception thrown from a nested action. The type of the 35 | scoped variable is the type of the exception thrown. 36 | 37 | var 38 | false 39 | false 40 | 41 | 42 | 43 | 44 | 45 | Simple conditional tag that establishes a context for 46 | mutually exclusive conditional operations, marked by 47 | <when> and <otherwise> 48 | 49 | choose 50 | org.apache.taglibs.standard.tag.common.core.ChooseTag 51 | JSP 52 | 53 | 54 | 55 | 56 | Simple conditional tag, which evalutes its body if the 57 | supplied condition is true and optionally exposes a Boolean 58 | scripting variable representing the evaluation of this condition 59 | 60 | if 61 | org.apache.taglibs.standard.tag.rt.core.IfTag 62 | JSP 63 | 64 | 65 | The test condition that determines whether or 66 | not the body content should be processed. 67 | 68 | test 69 | true 70 | true 71 | boolean 72 | 73 | 74 | 75 | Name of the exported scoped variable for the 76 | resulting value of the test condition. The type 77 | of the scoped variable is Boolean. 78 | 79 | var 80 | false 81 | false 82 | 83 | 84 | 85 | Scope for var. 86 | 87 | scope 88 | false 89 | false 90 | 91 | 92 | 93 | 94 | 95 | Retrieves an absolute or relative URL and exposes its contents 96 | to either the page, a String in 'var', or a Reader in 'varReader'. 97 | 98 | import 99 | org.apache.taglibs.standard.tag.rt.core.ImportTag 100 | org.apache.taglibs.standard.tei.ImportTEI 101 | JSP 102 | 103 | 104 | The URL of the resource to import. 105 | 106 | url 107 | true 108 | true 109 | 110 | 111 | 112 | Name of the exported scoped variable for the 113 | resource's content. The type of the scoped 114 | variable is String. 115 | 116 | var 117 | false 118 | false 119 | 120 | 121 | 122 | Scope for var. 123 | 124 | scope 125 | false 126 | false 127 | 128 | 129 | 130 | Name of the exported scoped variable for the 131 | resource's content. The type of the scoped 132 | variable is Reader. 133 | 134 | varReader 135 | false 136 | false 137 | 138 | 139 | 140 | Name of the context when accessing a relative 141 | URL resource that belongs to a foreign 142 | context. 143 | 144 | context 145 | false 146 | true 147 | 148 | 149 | 150 | Character encoding of the content at the input 151 | resource. 152 | 153 | charEncoding 154 | false 155 | true 156 | 157 | 158 | 159 | 160 | 161 | The basic iteration tag, accepting many different 162 | collection types and supporting subsetting and other 163 | functionality 164 | 165 | forEach 166 | org.apache.taglibs.standard.tag.rt.core.ForEachTag 167 | org.apache.taglibs.standard.tei.ForEachTEI 168 | JSP 169 | 170 | 171 | Collection of items to iterate over. 172 | 173 | items 174 | false 175 | true 176 | java.lang.Object 177 | 178 | java.lang.Object 179 | 180 | 181 | 182 | 183 | If items specified: 184 | Iteration begins at the item located at the 185 | specified index. First item of the collection has 186 | index 0. 187 | If items not specified: 188 | Iteration begins with index set at the value 189 | specified. 190 | 191 | begin 192 | false 193 | true 194 | int 195 | 196 | 197 | 198 | If items specified: 199 | Iteration ends at the item located at the 200 | specified index (inclusive). 201 | If items not specified: 202 | Iteration ends when index reaches the value 203 | specified. 204 | 205 | end 206 | false 207 | true 208 | int 209 | 210 | 211 | 212 | Iteration will only process every step items of 213 | the collection, starting with the first one. 214 | 215 | step 216 | false 217 | true 218 | int 219 | 220 | 221 | 222 | Name of the exported scoped variable for the 223 | current item of the iteration. This scoped 224 | variable has nested visibility. Its type depends 225 | on the object of the underlying collection. 226 | 227 | var 228 | false 229 | false 230 | 231 | 232 | 233 | Name of the exported scoped variable for the 234 | status of the iteration. Object exported is of type 235 | javax.servlet.jsp.jstl.core.LoopTagStatus. This scoped variable has nested 236 | visibility. 237 | 238 | varStatus 239 | false 240 | false 241 | 242 | 243 | 244 | 245 | 246 | Iterates over tokens, separated by the supplied delimeters 247 | 248 | forTokens 249 | org.apache.taglibs.standard.tag.rt.core.ForTokensTag 250 | JSP 251 | 252 | 253 | String of tokens to iterate over. 254 | 255 | items 256 | true 257 | true 258 | java.lang.String 259 | 260 | java.lang.String 261 | 262 | 263 | 264 | 265 | The set of delimiters (the characters that 266 | separate the tokens in the string). 267 | 268 | delims 269 | true 270 | true 271 | java.lang.String 272 | 273 | 274 | 275 | Iteration begins at the token located at the 276 | specified index. First token has index 0. 277 | 278 | begin 279 | false 280 | true 281 | int 282 | 283 | 284 | 285 | Iteration ends at the token located at the 286 | specified index (inclusive). 287 | 288 | end 289 | false 290 | true 291 | int 292 | 293 | 294 | 295 | Iteration will only process every step tokens 296 | of the string, starting with the first one. 297 | 298 | step 299 | false 300 | true 301 | int 302 | 303 | 304 | 305 | Name of the exported scoped variable for the 306 | current item of the iteration. This scoped 307 | variable has nested visibility. 308 | 309 | var 310 | false 311 | false 312 | 313 | 314 | 315 | Name of the exported scoped variable for the 316 | status of the iteration. Object exported is of 317 | type 318 | javax.servlet.jsp.jstl.core.LoopTag 319 | Status. This scoped variable has nested 320 | visibility. 321 | 322 | varStatus 323 | false 324 | false 325 | 326 | 327 | 328 | 329 | 330 | Like <%= ... >, but for expressions. 331 | 332 | out 333 | org.apache.taglibs.standard.tag.rt.core.OutTag 334 | JSP 335 | 336 | 337 | Expression to be evaluated. 338 | 339 | value 340 | true 341 | true 342 | 343 | 344 | 345 | Default value if the resulting value is null. 346 | 347 | default 348 | false 349 | true 350 | 351 | 352 | 353 | Determines whether characters <,>,&,'," in the 354 | resulting string should be converted to their 355 | corresponding character entity codes. Default value is 356 | true. 357 | 358 | escapeXml 359 | false 360 | true 361 | 362 | 363 | 364 | 365 | 366 | 367 | Subtag of <choose> that follows <when> tags 368 | and runs only if all of the prior conditions evaluated to 369 | 'false' 370 | 371 | otherwise 372 | org.apache.taglibs.standard.tag.common.core.OtherwiseTag 373 | JSP 374 | 375 | 376 | 377 | 378 | Adds a parameter to a containing 'import' tag's URL. 379 | 380 | param 381 | org.apache.taglibs.standard.tag.rt.core.ParamTag 382 | JSP 383 | 384 | 385 | Name of the query string parameter. 386 | 387 | name 388 | true 389 | true 390 | 391 | 392 | 393 | Value of the parameter. 394 | 395 | value 396 | false 397 | true 398 | 399 | 400 | 401 | 402 | 403 | Redirects to a new URL. 404 | 405 | redirect 406 | org.apache.taglibs.standard.tag.rt.core.RedirectTag 407 | JSP 408 | 409 | 410 | The URL of the resource to redirect to. 411 | 412 | url 413 | false 414 | true 415 | 416 | 417 | 418 | Name of the context when redirecting to a relative URL 419 | resource that belongs to a foreign context. 420 | 421 | context 422 | false 423 | true 424 | 425 | 426 | 427 | 428 | 429 | Removes a scoped variable (from a particular scope, if specified). 430 | 431 | remove 432 | org.apache.taglibs.standard.tag.common.core.RemoveTag 433 | empty 434 | 435 | 436 | Name of the scoped variable to be removed. 437 | 438 | var 439 | true 440 | false 441 | 442 | 443 | 444 | Scope for var. 445 | 446 | scope 447 | false 448 | false 449 | 450 | 451 | 452 | 453 | 454 | Sets the result of an expression evaluation in a 'scope' 455 | 456 | set 457 | org.apache.taglibs.standard.tag.rt.core.SetTag 458 | JSP 459 | 460 | 461 | Name of the exported scoped variable to hold the value 462 | specified in the action. The type of the scoped variable is 463 | whatever type the value expression evaluates to. 464 | 465 | var 466 | false 467 | false 468 | 469 | 470 | 471 | Expression to be evaluated. 472 | 473 | value 474 | false 475 | true 476 | 477 | java.lang.Object 478 | 479 | 480 | 481 | 482 | Target object whose property will be set. Must evaluate to 483 | a JavaBeans object with setter property property, or to a 484 | java.util.Map object. 485 | 486 | target 487 | false 488 | true 489 | 490 | 491 | 492 | Name of the property to be set in the target object. 493 | 494 | property 495 | false 496 | true 497 | 498 | 499 | 500 | Scope for var. 501 | 502 | scope 503 | false 504 | false 505 | 506 | 507 | 508 | 509 | 510 | Creates a URL with optional query parameters. 511 | 512 | url 513 | org.apache.taglibs.standard.tag.rt.core.UrlTag 514 | JSP 515 | 516 | 517 | Name of the exported scoped variable for the 518 | processed url. The type of the scoped variable is 519 | String. 520 | 521 | var 522 | false 523 | false 524 | 525 | 526 | 527 | Scope for var. 528 | 529 | scope 530 | false 531 | false 532 | 533 | 534 | 535 | URL to be processed. 536 | 537 | value 538 | false 539 | true 540 | 541 | 542 | 543 | Name of the context when specifying a relative URL 544 | resource that belongs to a foreign context. 545 | 546 | context 547 | false 548 | true 549 | 550 | 551 | 552 | 553 | 554 | Subtag of <choose> that includes its body if its 555 | condition evalutes to 'true' 556 | 557 | when 558 | org.apache.taglibs.standard.tag.rt.core.WhenTag 559 | JSP 560 | 561 | 562 | The test condition that determines whether or not the 563 | body content should be processed. 564 | 565 | test 566 | true 567 | true 568 | boolean 569 | 570 | 571 | 572 | 573 | -------------------------------------------------------------------------------- /web-app/WEB-INF/tld/fmt.tld: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | JSTL 1.2 i18n-capable formatting library 9 | JSTL fmt 10 | 1.2 11 | fmt 12 | http://java.sun.com/jsp/jstl/fmt 13 | 14 | 15 | 16 | Provides core validation features for JSTL tags. 17 | 18 | 19 | org.apache.taglibs.standard.tlv.JstlFmtTLV 20 | 21 | 22 | 23 | 24 | 25 | Sets the request character encoding 26 | 27 | requestEncoding 28 | org.apache.taglibs.standard.tag.rt.fmt.RequestEncodingTag 29 | empty 30 | 31 | 32 | Name of character encoding to be applied when 33 | decoding request parameters. 34 | 35 | value 36 | false 37 | true 38 | 39 | 40 | 41 | 42 | 43 | Stores the given locale in the locale configuration variable 44 | 45 | setLocale 46 | org.apache.taglibs.standard.tag.rt.fmt.SetLocaleTag 47 | empty 48 | 49 | 50 | A String value is interpreted as the 51 | printable representation of a locale, which 52 | must contain a two-letter (lower-case) 53 | language code (as defined by ISO-639), 54 | and may contain a two-letter (upper-case) 55 | country code (as defined by ISO-3166). 56 | Language and country codes must be 57 | separated by hyphen (-) or underscore 58 | (_). 59 | 60 | value 61 | true 62 | true 63 | 64 | 65 | 66 | Vendor- or browser-specific variant. 67 | See the java.util.Locale javadocs for 68 | more information on variants. 69 | 70 | variant 71 | false 72 | true 73 | 74 | 75 | 76 | Scope of the locale configuration variable. 77 | 78 | scope 79 | false 80 | false 81 | 82 | 83 | 84 | 85 | 86 | Specifies the time zone for any time formatting or parsing actions 87 | nested in its body 88 | 89 | timeZone 90 | org.apache.taglibs.standard.tag.rt.fmt.TimeZoneTag 91 | JSP 92 | 93 | 94 | The time zone. A String value is interpreted as 95 | a time zone ID. This may be one of the time zone 96 | IDs supported by the Java platform (such as 97 | "America/Los_Angeles") or a custom time zone 98 | ID (such as "GMT-8"). See 99 | java.util.TimeZone for more information on 100 | supported time zone formats. 101 | 102 | value 103 | true 104 | true 105 | 106 | 107 | 108 | 109 | 110 | Stores the given time zone in the time zone configuration variable 111 | 112 | setTimeZone 113 | org.apache.taglibs.standard.tag.rt.fmt.SetTimeZoneTag 114 | empty 115 | 116 | 117 | The time zone. A String value is interpreted as 118 | a time zone ID. This may be one of the time zone 119 | IDs supported by the Java platform (such as 120 | "America/Los_Angeles") or a custom time zone 121 | ID (such as "GMT-8"). See java.util.TimeZone for 122 | more information on supported time zone 123 | formats. 124 | 125 | value 126 | true 127 | true 128 | 129 | 130 | 131 | Name of the exported scoped variable which 132 | stores the time zone of type 133 | java.util.TimeZone. 134 | 135 | var 136 | false 137 | false 138 | 139 | 140 | 141 | Scope of var or the time zone configuration 142 | variable. 143 | 144 | scope 145 | false 146 | false 147 | 148 | 149 | 150 | 151 | 152 | Loads a resource bundle to be used by its tag body 153 | 154 | bundle 155 | org.apache.taglibs.standard.tag.rt.fmt.BundleTag 156 | JSP 157 | 158 | 159 | Resource bundle base name. This is the bundle's 160 | fully-qualified resource name, which has the same 161 | form as a fully-qualified class name, that is, it uses 162 | "." as the package component separator and does not 163 | have any file type (such as ".class" or ".properties") 164 | suffix. 165 | 166 | basename 167 | true 168 | true 169 | 170 | 171 | 172 | Prefix to be prepended to the value of the message 173 | key of any nested <fmt:message> action. 174 | 175 | prefix 176 | false 177 | true 178 | 179 | 180 | 181 | 182 | 183 | Loads a resource bundle and stores it in the named scoped variable or 184 | the bundle configuration variable 185 | 186 | setBundle 187 | org.apache.taglibs.standard.tag.rt.fmt.SetBundleTag 188 | empty 189 | 190 | 191 | Resource bundle base name. This is the bundle's 192 | fully-qualified resource name, which has the same 193 | form as a fully-qualified class name, that is, it uses 194 | "." as the package component separator and does not 195 | have any file type (such as ".class" or ".properties") 196 | suffix. 197 | 198 | basename 199 | true 200 | true 201 | 202 | 203 | 204 | Name of the exported scoped variable which stores 205 | the i18n localization context of type 206 | javax.servlet.jsp.jstl.fmt.LocalizationC 207 | ontext. 208 | 209 | var 210 | false 211 | false 212 | 213 | 214 | 215 | Scope of var or the localization context 216 | configuration variable. 217 | 218 | scope 219 | false 220 | false 221 | 222 | 223 | 224 | 225 | 226 | Maps key to localized message and performs parametric replacement 227 | 228 | message 229 | org.apache.taglibs.standard.tag.rt.fmt.MessageTag 230 | JSP 231 | 232 | 233 | Message key to be looked up. 234 | 235 | key 236 | false 237 | true 238 | 239 | 240 | 241 | Localization context in whose resource 242 | bundle the message key is looked up. 243 | 244 | bundle 245 | false 246 | true 247 | 248 | 249 | 250 | Name of the exported scoped variable 251 | which stores the localized message. 252 | 253 | var 254 | false 255 | false 256 | 257 | 258 | 259 | Scope of var. 260 | 261 | scope 262 | false 263 | false 264 | 265 | 266 | 267 | 268 | 269 | Supplies an argument for parametric replacement to a containing 270 | <message> tag 271 | 272 | param 273 | org.apache.taglibs.standard.tag.rt.fmt.ParamTag 274 | JSP 275 | 276 | 277 | Argument used for parametric replacement. 278 | 279 | value 280 | false 281 | true 282 | 283 | 284 | 285 | 286 | 287 | Formats a numeric value as a number, currency, or percentage 288 | 289 | formatNumber 290 | org.apache.taglibs.standard.tag.rt.fmt.FormatNumberTag 291 | JSP 292 | 293 | 294 | Numeric value to be formatted. 295 | 296 | value 297 | false 298 | true 299 | 300 | 301 | 302 | Specifies whether the value is to be 303 | formatted as number, currency, or 304 | percentage. 305 | 306 | type 307 | false 308 | true 309 | 310 | 311 | 312 | Custom formatting pattern. 313 | 314 | pattern 315 | false 316 | true 317 | 318 | 319 | 320 | ISO 4217 currency code. Applied only 321 | when formatting currencies (i.e. if type is 322 | equal to "currency"); ignored otherwise. 323 | 324 | currencyCode 325 | false 326 | true 327 | 328 | 329 | 330 | Currency symbol. Applied only when 331 | formatting currencies (i.e. if type is equal 332 | to "currency"); ignored otherwise. 333 | 334 | currencySymbol 335 | false 336 | true 337 | 338 | 339 | 340 | Specifies whether the formatted output 341 | will contain any grouping separators. 342 | 343 | groupingUsed 344 | false 345 | true 346 | 347 | 348 | 349 | Maximum number of digits in the integer 350 | portion of the formatted output. 351 | 352 | maxIntegerDigits 353 | false 354 | true 355 | 356 | 357 | 358 | Minimum number of digits in the integer 359 | portion of the formatted output. 360 | 361 | minIntegerDigits 362 | false 363 | true 364 | 365 | 366 | 367 | Maximum number of digits in the 368 | fractional portion of the formatted output. 369 | 370 | maxFractionDigits 371 | false 372 | true 373 | 374 | 375 | 376 | Minimum number of digits in the 377 | fractional portion of the formatted output. 378 | 379 | minFractionDigits 380 | false 381 | true 382 | 383 | 384 | 385 | Name of the exported scoped variable 386 | which stores the formatted result as a 387 | String. 388 | 389 | var 390 | false 391 | false 392 | 393 | 394 | 395 | Scope of var. 396 | 397 | scope 398 | false 399 | false 400 | 401 | 402 | 403 | 404 | 405 | Parses the string representation of a number, currency, or percentage 406 | 407 | parseNumber 408 | org.apache.taglibs.standard.tag.rt.fmt.ParseNumberTag 409 | JSP 410 | 411 | 412 | String to be parsed. 413 | 414 | value 415 | false 416 | true 417 | 418 | 419 | 420 | Specifies whether the string in the value 421 | attribute should be parsed as a number, 422 | currency, or percentage. 423 | 424 | type 425 | false 426 | true 427 | 428 | 429 | 430 | Custom formatting pattern that determines 431 | how the string in the value attribute is to be 432 | parsed. 433 | 434 | pattern 435 | false 436 | true 437 | 438 | 439 | 440 | Locale whose default formatting pattern (for 441 | numbers, currencies, or percentages, 442 | respectively) is to be used during the parse 443 | operation, or to which the pattern specified 444 | via the pattern attribute (if present) is 445 | applied. 446 | 447 | parseLocale 448 | false 449 | true 450 | 451 | 452 | 453 | Specifies whether just the integer portion of 454 | the given value should be parsed. 455 | 456 | integerOnly 457 | false 458 | true 459 | 460 | 461 | 462 | Name of the exported scoped variable which 463 | stores the parsed result (of type 464 | java.lang.Number). 465 | 466 | var 467 | false 468 | false 469 | 470 | 471 | 472 | Scope of var. 473 | 474 | scope 475 | false 476 | false 477 | 478 | 479 | 480 | 481 | 482 | Formats a date and/or time using the supplied styles and pattern 483 | 484 | formatDate 485 | org.apache.taglibs.standard.tag.rt.fmt.FormatDateTag 486 | empty 487 | 488 | 489 | Date and/or time to be formatted. 490 | 491 | value 492 | true 493 | true 494 | 495 | 496 | 497 | Specifies whether the time, the date, or both 498 | the time and date components of the given 499 | date are to be formatted. 500 | 501 | type 502 | false 503 | true 504 | 505 | 506 | 507 | Predefined formatting style for dates. Follows 508 | the semantics defined in class 509 | java.text.DateFormat. Applied only 510 | when formatting a date or both a date and 511 | time (i.e. if type is missing or is equal to 512 | "date" or "both"); ignored otherwise. 513 | 514 | dateStyle 515 | false 516 | true 517 | 518 | 519 | 520 | Predefined formatting style for times. Follows 521 | the semantics defined in class 522 | java.text.DateFormat. Applied only 523 | when formatting a time or both a date and 524 | time (i.e. if type is equal to "time" or "both"); 525 | ignored otherwise. 526 | 527 | timeStyle 528 | false 529 | true 530 | 531 | 532 | 533 | Custom formatting style for dates and times. 534 | 535 | pattern 536 | false 537 | true 538 | 539 | 540 | 541 | Time zone in which to represent the formatted 542 | time. 543 | 544 | timeZone 545 | false 546 | true 547 | 548 | 549 | 550 | Name of the exported scoped variable which 551 | stores the formatted result as a String. 552 | 553 | var 554 | false 555 | false 556 | 557 | 558 | 559 | Scope of var. 560 | 561 | scope 562 | false 563 | false 564 | 565 | 566 | 567 | 568 | 569 | Parses the string representation of a date and/or time 570 | 571 | parseDate 572 | org.apache.taglibs.standard.tag.rt.fmt.ParseDateTag 573 | JSP 574 | 575 | 576 | Date string to be parsed. 577 | 578 | value 579 | false 580 | true 581 | 582 | 583 | 584 | Specifies whether the date string in the 585 | value attribute is supposed to contain a 586 | time, a date, or both. 587 | 588 | type 589 | false 590 | true 591 | 592 | 593 | 594 | Predefined formatting style for days 595 | which determines how the date 596 | component of the date string is to be 597 | parsed. Applied only when formatting a 598 | date or both a date and time (i.e. if type 599 | is missing or is equal to "date" or "both"); 600 | ignored otherwise. 601 | 602 | dateStyle 603 | false 604 | true 605 | 606 | 607 | 608 | Predefined formatting styles for times 609 | which determines how the time 610 | component in the date string is to be 611 | parsed. Applied only when formatting a 612 | time or both a date and time (i.e. if type 613 | is equal to "time" or "both"); ignored 614 | otherwise. 615 | 616 | timeStyle 617 | false 618 | true 619 | 620 | 621 | 622 | Custom formatting pattern which 623 | determines how the date string is to be 624 | parsed. 625 | 626 | pattern 627 | false 628 | true 629 | 630 | 631 | 632 | Time zone in which to interpret any time 633 | information in the date string. 634 | 635 | timeZone 636 | false 637 | true 638 | 639 | 640 | 641 | Locale whose predefined formatting styles 642 | for dates and times are to be used during 643 | the parse operation, or to which the 644 | pattern specified via the pattern 645 | attribute (if present) is applied. 646 | 647 | parseLocale 648 | false 649 | true 650 | 651 | 652 | 653 | Name of the exported scoped variable in 654 | which the parsing result (of type 655 | java.util.Date) is stored. 656 | 657 | var 658 | false 659 | false 660 | 661 | 662 | 663 | Scope of var. 664 | 665 | scope 666 | false 667 | false 668 | 669 | 670 | 671 | 672 | -------------------------------------------------------------------------------- /web-app/WEB-INF/tld/grails.tld: -------------------------------------------------------------------------------- 1 | 2 | 7 | The Grails custom tag library 8 | 0.2 9 | grails 10 | http://grails.codehaus.org/tags 11 | 12 | 13 | link 14 | org.codehaus.groovy.grails.web.taglib.jsp.JspLinkTag 15 | JSP 16 | 17 | action 18 | false 19 | true 20 | 21 | 22 | controller 23 | false 24 | true 25 | 26 | 27 | id 28 | false 29 | true 30 | 31 | 32 | url 33 | false 34 | true 35 | 36 | 37 | params 38 | false 39 | true 40 | 41 | true 42 | 43 | 44 | form 45 | org.codehaus.groovy.grails.web.taglib.jsp.JspFormTag 46 | JSP 47 | 48 | action 49 | false 50 | true 51 | 52 | 53 | controller 54 | false 55 | true 56 | 57 | 58 | id 59 | false 60 | true 61 | 62 | 63 | url 64 | false 65 | true 66 | 67 | 68 | method 69 | true 70 | true 71 | 72 | true 73 | 74 | 75 | select 76 | org.codehaus.groovy.grails.web.taglib.jsp.JspSelectTag 77 | JSP 78 | 79 | name 80 | true 81 | true 82 | 83 | 84 | value 85 | false 86 | true 87 | 88 | 89 | optionKey 90 | false 91 | true 92 | 93 | 94 | optionValue 95 | false 96 | true 97 | 98 | true 99 | 100 | 101 | datePicker 102 | org.codehaus.groovy.grails.web.taglib.jsp.JspDatePickerTag 103 | empty 104 | 105 | name 106 | true 107 | true 108 | 109 | 110 | value 111 | false 112 | true 113 | 114 | 115 | precision 116 | false 117 | true 118 | 119 | false 120 | 121 | 122 | currencySelect 123 | org.codehaus.groovy.grails.web.taglib.jsp.JspCurrencySelectTag 124 | empty 125 | 126 | name 127 | true 128 | true 129 | 130 | 131 | value 132 | false 133 | true 134 | 135 | true 136 | 137 | 138 | localeSelect 139 | org.codehaus.groovy.grails.web.taglib.jsp.JspLocaleSelectTag 140 | empty 141 | 142 | name 143 | true 144 | true 145 | 146 | 147 | value 148 | false 149 | true 150 | 151 | true 152 | 153 | 154 | timeZoneSelect 155 | org.codehaus.groovy.grails.web.taglib.jsp.JspTimeZoneSelectTag 156 | empty 157 | 158 | name 159 | true 160 | true 161 | 162 | 163 | value 164 | false 165 | true 166 | 167 | true 168 | 169 | 170 | checkBox 171 | org.codehaus.groovy.grails.web.taglib.jsp.JspCheckboxTag 172 | empty 173 | 174 | name 175 | true 176 | true 177 | 178 | 179 | value 180 | true 181 | true 182 | 183 | true 184 | 185 | 186 | hasErrors 187 | org.codehaus.groovy.grails.web.taglib.jsp.JspHasErrorsTag 188 | JSP 189 | 190 | model 191 | false 192 | true 193 | 194 | 195 | bean 196 | false 197 | true 198 | 199 | 200 | field 201 | false 202 | true 203 | 204 | false 205 | 206 | 207 | eachError 208 | org.codehaus.groovy.grails.web.taglib.jsp.JspEachErrorTag 209 | JSP 210 | 211 | model 212 | false 213 | true 214 | 215 | 216 | bean 217 | false 218 | true 219 | 220 | 221 | field 222 | false 223 | true 224 | 225 | false 226 | 227 | 228 | renderErrors 229 | org.codehaus.groovy.grails.web.taglib.jsp.JspEachErrorTag 230 | JSP 231 | 232 | model 233 | false 234 | true 235 | 236 | 237 | bean 238 | false 239 | true 240 | 241 | 242 | field 243 | false 244 | true 245 | 246 | 247 | as 248 | true 249 | true 250 | 251 | false 252 | 253 | 254 | message 255 | org.codehaus.groovy.grails.web.taglib.jsp.JspMessageTag 256 | JSP 257 | 258 | code 259 | false 260 | true 261 | 262 | 263 | error 264 | false 265 | true 266 | 267 | 268 | default 269 | false 270 | true 271 | 272 | false 273 | 274 | 275 | remoteFunction 276 | org.codehaus.groovy.grails.web.taglib.jsp.JspRemoteFunctionTag 277 | empty 278 | 279 | before 280 | false 281 | true 282 | 283 | 284 | after 285 | false 286 | true 287 | 288 | 289 | action 290 | false 291 | true 292 | 293 | 294 | controller 295 | false 296 | true 297 | 298 | 299 | id 300 | false 301 | true 302 | 303 | 304 | url 305 | false 306 | true 307 | 308 | 309 | params 310 | false 311 | true 312 | 313 | 314 | asynchronous 315 | false 316 | true 317 | 318 | 319 | method 320 | false 321 | true 322 | 323 | 324 | update 325 | false 326 | true 327 | 328 | 329 | onSuccess 330 | false 331 | true 332 | 333 | 334 | onFailure 335 | false 336 | true 337 | 338 | 339 | onComplete 340 | false 341 | true 342 | 343 | 344 | onLoading 345 | false 346 | true 347 | 348 | 349 | onLoaded 350 | false 351 | true 352 | 353 | 354 | onInteractive 355 | false 356 | true 357 | 358 | true 359 | 360 | 361 | remoteLink 362 | org.codehaus.groovy.grails.web.taglib.jsp.JspRemoteLinkTag 363 | JSP 364 | 365 | before 366 | false 367 | true 368 | 369 | 370 | after 371 | false 372 | true 373 | 374 | 375 | action 376 | false 377 | true 378 | 379 | 380 | controller 381 | false 382 | true 383 | 384 | 385 | id 386 | false 387 | true 388 | 389 | 390 | url 391 | false 392 | true 393 | 394 | 395 | params 396 | false 397 | true 398 | 399 | 400 | asynchronous 401 | false 402 | true 403 | 404 | 405 | method 406 | false 407 | true 408 | 409 | 410 | update 411 | false 412 | true 413 | 414 | 415 | onSuccess 416 | false 417 | true 418 | 419 | 420 | onFailure 421 | false 422 | true 423 | 424 | 425 | onComplete 426 | false 427 | true 428 | 429 | 430 | onLoading 431 | false 432 | true 433 | 434 | 435 | onLoaded 436 | false 437 | true 438 | 439 | 440 | onInteractive 441 | false 442 | true 443 | 444 | true 445 | 446 | 447 | formRemote 448 | org.codehaus.groovy.grails.web.taglib.jsp.JspFormRemoteTag 449 | JSP 450 | 451 | before 452 | false 453 | true 454 | 455 | 456 | after 457 | false 458 | true 459 | 460 | 461 | action 462 | false 463 | true 464 | 465 | 466 | controller 467 | false 468 | true 469 | 470 | 471 | id 472 | false 473 | true 474 | 475 | 476 | url 477 | false 478 | true 479 | 480 | 481 | params 482 | false 483 | true 484 | 485 | 486 | asynchronous 487 | false 488 | true 489 | 490 | 491 | method 492 | false 493 | true 494 | 495 | 496 | update 497 | false 498 | true 499 | 500 | 501 | onSuccess 502 | false 503 | true 504 | 505 | 506 | onFailure 507 | false 508 | true 509 | 510 | 511 | onComplete 512 | false 513 | true 514 | 515 | 516 | onLoading 517 | false 518 | true 519 | 520 | 521 | onLoaded 522 | false 523 | true 524 | 525 | 526 | onInteractive 527 | false 528 | true 529 | 530 | true 531 | 532 | 533 | invokeTag 534 | org.codehaus.groovy.grails.web.taglib.jsp.JspInvokeGrailsTagLibTag 535 | JSP 536 | 537 | it 538 | java.lang.Object 539 | true 540 | NESTED 541 | 542 | 543 | tagName 544 | true 545 | true 546 | 547 | true 548 | 549 | 550 | 551 | -------------------------------------------------------------------------------- /web-app/WEB-INF/tld/spring.tld: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1.1.1 7 | 8 | 1.2 9 | 10 | Spring 11 | 12 | http://www.springframework.org/tags 13 | 14 | Spring Framework JSP Tag Library. Authors: Rod Johnson, Juergen Hoeller 15 | 16 | 17 | 18 | 19 | htmlEscape 20 | org.springframework.web.servlet.tags.HtmlEscapeTag 21 | JSP 22 | 23 | 24 | Sets default HTML escape value for the current page. 25 | Overrides a "defaultHtmlEscape" context-param in web.xml, if any. 26 | 27 | 28 | 29 | defaultHtmlEscape 30 | true 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | escapeBody 40 | org.springframework.web.servlet.tags.EscapeBodyTag 41 | JSP 42 | 43 | 44 | Escapes its enclosed body content, applying HTML escaping and/or JavaScript escaping. 45 | The HTML escaping flag participates in a page-wide or application-wide setting 46 | (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). 47 | 48 | 49 | 50 | htmlEscape 51 | false 52 | true 53 | 54 | 55 | 56 | javaScriptEscape 57 | false 58 | true 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | message 67 | org.springframework.web.servlet.tags.MessageTag 68 | JSP 69 | 70 | 71 | Retrieves the message with the given code, or text if code isn't resolvable. 72 | The HTML escaping flag participates in a page-wide or application-wide setting 73 | (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). 74 | 75 | 76 | 77 | code 78 | false 79 | true 80 | 81 | 82 | 83 | arguments 84 | false 85 | true 86 | 87 | 88 | 89 | text 90 | false 91 | true 92 | 93 | 94 | 95 | var 96 | false 97 | true 98 | 99 | 100 | 101 | scope 102 | false 103 | true 104 | 105 | 106 | 107 | htmlEscape 108 | false 109 | true 110 | 111 | 112 | 113 | javaScriptEscape 114 | false 115 | true 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | theme 124 | org.springframework.web.servlet.tags.ThemeTag 125 | JSP 126 | 127 | 128 | Retrieves the theme message with the given code, or text if code isn't resolvable. 129 | The HTML escaping flag participates in a page-wide or application-wide setting 130 | (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). 131 | 132 | 133 | 134 | code 135 | false 136 | true 137 | 138 | 139 | 140 | arguments 141 | false 142 | true 143 | 144 | 145 | 146 | text 147 | false 148 | true 149 | 150 | 151 | 152 | var 153 | false 154 | true 155 | 156 | 157 | 158 | scope 159 | false 160 | true 161 | 162 | 163 | 164 | htmlEscape 165 | false 166 | true 167 | 168 | 169 | 170 | javaScriptEscape 171 | false 172 | true 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | hasBindErrors 181 | org.springframework.web.servlet.tags.BindErrorsTag 182 | JSP 183 | 184 | 185 | Provides Errors instance in case of bind errors. 186 | The HTML escaping flag participates in a page-wide or application-wide setting 187 | (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). 188 | 189 | 190 | 191 | errors 192 | org.springframework.validation.Errors 193 | 194 | 195 | 196 | name 197 | true 198 | true 199 | 200 | 201 | 202 | htmlEscape 203 | false 204 | true 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | nestedPath 213 | org.springframework.web.servlet.tags.NestedPathTag 214 | JSP 215 | 216 | 217 | Sets a nested path to be used by the bind tag's path. 218 | 219 | 220 | 221 | nestedPath 222 | java.lang.String 223 | 224 | 225 | 226 | path 227 | true 228 | true 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | bind 237 | org.springframework.web.servlet.tags.BindTag 238 | JSP 239 | 240 | 241 | Provides BindStatus object for the given bind path. 242 | The HTML escaping flag participates in a page-wide or application-wide setting 243 | (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). 244 | 245 | 246 | 247 | status 248 | org.springframework.web.servlet.support.BindStatus 249 | 250 | 251 | 252 | path 253 | true 254 | true 255 | 256 | 257 | 258 | ignoreNestedPath 259 | false 260 | true 261 | 262 | 263 | 264 | htmlEscape 265 | false 266 | true 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | transform 275 | org.springframework.web.servlet.tags.TransformTag 276 | JSP 277 | 278 | 279 | Provides transformation of variables to Strings, using an appropriate 280 | custom PropertyEditor from BindTag (can only be used inside BindTag). 281 | The HTML escaping flag participates in a page-wide or application-wide setting 282 | (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). 283 | 284 | 285 | 286 | value 287 | true 288 | true 289 | 290 | 291 | 292 | var 293 | false 294 | true 295 | 296 | 297 | 298 | scope 299 | false 300 | true 301 | 302 | 303 | 304 | htmlEscape 305 | false 306 | true 307 | 308 | 309 | 310 | 311 | 312 | --------------------------------------------------------------------------------