├── .gitignore ├── .travis.yml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── grails-app ├── conf │ ├── application.yml │ └── logback.groovy └── init │ └── pl │ └── touk │ └── excel │ └── export │ └── Application.groovy └── src ├── main └── groovy │ └── pl │ └── touk │ └── excel │ └── export │ ├── ExcelExportGrailsPlugin.groovy │ ├── Formatters.groovy │ ├── WebXlsxExporter.groovy │ ├── XlsxExporter.groovy │ ├── abilities │ ├── CellManipulationAbility.groovy │ ├── FileManipulationAbility.groovy │ └── RowManipulationAbility.groovy │ ├── getters │ ├── AsIsPropertyGetter.groovy │ ├── Getter.groovy │ ├── LongToDatePropertyGetter.groovy │ ├── MessageFromPropertyGetter.groovy │ └── PropertyGetter.groovy │ └── multisheet │ ├── AdditionalSheet.groovy │ └── SheetManipulator.groovy └── test └── groovy └── pl └── touk └── excel └── export ├── SampleObject.groovy ├── SampleObjectWithList.groovy ├── WebXlsxExporterSpec.groovy ├── XlsxExporterCreationSpec.groovy ├── XlsxExporterDateStyleSpec.groovy ├── XlsxExporterHeaderSpec.groovy ├── XlsxExporterManipulateCellsSpec.groovy ├── XlsxExporterMultipleSheetSpec.groovy ├── XlsxExporterRowSpec.groovy ├── XlsxExporterSpec.groovy ├── XlsxTestOnTemporaryFolder.groovy └── getters └── PropertyGetterSpec.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | .DS_Store 3 | .gradle 4 | build/ 5 | .idea 6 | *.iml 7 | *.ipr 8 | *.iws 9 | .project 10 | .settings 11 | .classpath 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: groovy 2 | jdk: 3 | - oraclejdk8 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the excel-export Grails plugin using [Apache POI](https://poi.apache.org/) 2 | 3 | [![Build Status](https://travis-ci.org/TouK/excel-export.svg?branch=master)](https://travis-ci.org/TouK/excel-export) [ ![Download](https://api.bintray.com/packages/grails-excel-export/plugins/excel-export/images/download.svg) ](https://bintray.com/grails-excel-export/plugins/excel-export/_latestVersion) 4 | 5 | # What does it do? 6 | 7 | This plugin allows for easy exporting of object lists to an Office Open XML workbook (Microsoft Excel 2007+ xlsx) file, while still allowing you to handle the export file on a cell-by-cell basis. 8 | 9 | An alternative to this plugin is James Kleeh's [groovy-excel-builder](https://github.com/jameskleeh/groovy-excel-builder) which provides a fantastic Groovy DSL with varying degress of fine-grained control. 10 | 11 | # When should I use it? 12 | 13 | There are two scenarios for which this plugin was created: 14 | 15 | 1. When you want to export data from your controllers for download and want to maintain full control of how you handle this data. 16 | 17 | 2. When your customer says: 'I want 100 reports in this new project' and nobody has any clue what those reports look like, you can use this plugin as a DSL, i.e. tell your client 'Hey, I've got good news. We have a nice DSL for you, so that you can write all those reports yourself. And it's free!' (or charge them anyway). 18 | 19 | In either case, you can export to either a file on disk or to the HTTP response output stream (download as xlsx). 20 | 21 | This plugin has been used like this in commercial projects. 22 | 23 | # How do I use it? 24 | 25 | Say, in your controller you have a list of objects, like so: 26 | 27 | ```groovy 28 | List products = productFactory.createProducts() 29 | ``` 30 | 31 | To export selected properties of those products to a file on disk, see the following example, where `withProperties` is a list of properties that are going to be exported to xlsx, in the given order: 32 | 33 | ```groovy 34 | def withProperties = ['name', 'description', 'validTill', 'productNumber', 'price.value'] 35 | new XlsxExporter('/tmp/myReportFile.xlsx'). 36 | add(products, withProperties). 37 | save() 38 | ``` 39 | 40 | Notice that you can use nested properties (e.g. `price.value`) of your objects. 41 | 42 | To add a header row to the spreadshet and make the file downloadable from a controller, you do this: 43 | 44 | ```groovy 45 | def headers = ['Name', 'Description', 'Valid Till', 'Product Number', 'Price'] 46 | def withProperties = ['name', 'description', 'validTill', 'productNumber', 'price.value'] 47 | 48 | new WebXlsxExporter().with { 49 | setResponseHeaders(response) 50 | fillHeader(headers) 51 | add(products, withProperties) 52 | save(response.outputStream) 53 | } 54 | ``` 55 | 56 | `WebXlsxExporter` is the same thing as `XlsxExporter`, just with the ability to handle HTTP response headers. 57 | 58 | You can also manipulate the file on a cell-by-cell basis: 59 | 60 | ```groovy 61 | new WebXlsxExporter().with { 62 | setResponseHeaders(response) 63 | fillRow(["aaa", "bbb", 13, new Date()], 1) 64 | fillRow(["ccc", "ddd", 87, new Date()], 2) 65 | putCellValue(3, 3, "Now I'm here") 66 | save(response.outputStream) 67 | } 68 | ``` 69 | 70 | You can also mix approaches (cell-by-cell, and object list): 71 | 72 | ```groovy 73 | def withProperties = ['name', 'description', 'validTill', 'productNumber', 'price.value'] 74 | 75 | new WebXlsxExporter().with { 76 | setResponseHeaders(response) 77 | fillRow(["aaa", "bbb", 13, new Date()], 1) 78 | fillRow(["ccc", "ddd", 87, new Date()], 2) 79 | putCellValue(3, 3, "Now I'm here") 80 | add(products, withProperties, 4) //NOTICE: we are adding objects starting from line 4 81 | save(response.outputStream) 82 | } 83 | ``` 84 | 85 | # What about multiple sheets? 86 | 87 | If you'd like to work with multiple sheets, just call `sheet(sheetName)` on your exporter. It returns an instance 88 | of `AdditionalSheet` that shares the same row/cell manipulation API as the exporter itself: 89 | 90 | ```groovy 91 | List products = productFactory.createProducts() 92 | def withProperties = ['name', 'description', 'validTill', 'productNumber', 'price.value'] 93 | 94 | new WebXlsxExporter().with { 95 | setResponseHeaders(response) print methods of controller 96 | sheet('second sheet').with { 97 | fillHeader(withProperties) 98 | add( products, withProperties ) 99 | } 100 | save(response.outputStream) 101 | } 102 | ``` 103 | 104 | You can also mix using additional sheets with a default sheet: 105 | 106 | ```groovy 107 | List products = productFactory.createProducts() 108 | def withProperties = ['name', 'description', 'validTill', 'productNumber', 'price.value'] 109 | 110 | new WebXlsxExporter().with { 111 | setResponseHeaders(response) 112 | fillHeader(withProperties) 113 | add( products, withProperties ) 114 | sheet('second sheet').with { 115 | fillHeader(withProperties) 116 | add( products, withProperties ) 117 | } 118 | save(response.outputStream) 119 | } 120 | ``` 121 | 122 | And if you'd like to change the name of default sheet, just set it before first call: 123 | 124 | ```groovy 125 | WebXlsxExporter webXlsxExporter = new WebXlsxExporter() 126 | webXlsxExporter.setWorksheetName("products") 127 | webXlsxExporter.with { 128 | ... 129 | } 130 | ``` 131 | 132 | # How to export my own types? 133 | 134 | This plugin handles basic property types pretty well (String, Date, Boolean, Timestamp, NullObject, Long, Integer, BigDecimal, BigInteger, Byte, Double, Float, Short). It also handles nested properties, and if everything fails, tries to call `toString()`. But sooner or later, you'll want to export a property of a different type the way you like it. 135 | What you need to write, is a Getter. Or, better, a `PropertyGetter`. It's super easy, here is example of one that takes Currency and turns it into a String: 136 | 137 | ```groovy 138 | class CurrencyGetter extends PropertyGetter { // From Currency, to String 139 | CurrencyGetter(String propertyName) { 140 | super(propertyName) 141 | } 142 | 143 | @Override 144 | protected String format(Currency value) { 145 | return value.displayName // you can do anything you like in here 146 | } 147 | } 148 | ``` 149 | 150 | The `format()` method allows you customize the value before the object is saved in an xlsx cell. 151 | 152 | And, of course, to use it, just add it into the `withProperties` list, like this: 153 | 154 | ```groovy 155 | def withProperties = ['name', new CurrencyGetter('price.currency'), 'price.value'] 156 | 157 | new WebXlsxExporter().with { 158 | setResponseHeaders(response) 159 | add(products, withProperties) 160 | save(response.outputStream) 161 | } 162 | ``` 163 | 164 | Of course we could have just used `currency.displayName` in `withProperties`, but you get the idea. 165 | 166 | There are two Getters ready for your convenience. 167 | 168 | `LongToDatePropertyGetter` gets a long and saves it as a date in xlsx, while `MessageFromPropertyGetter` handles i18n. Speaking of which... 169 | 170 | # How to i18n? 171 | 172 | To get i18n of headers in your controller, just use controller's existing message method: 173 | 174 | ```groovy 175 | def headers = [message(code: 'product.name.header'), 176 | message(code: 'product.description.header'), 177 | message(code: 'product.validTill.header'), 178 | message(code: 'product.productNumber.header'), 179 | message(code: 'price.value.header')] 180 | ``` 181 | 182 | You can do more though. To i18n values, use `MessageFromPropertyGetter`: 183 | 184 | ```groovy 185 | MessageSource messageSource //injected in the controller automatically by Grails, just declare it 186 | 187 | def export() { 188 | List products = productFactory.createProducts() 189 | 190 | def headers = ['name', 'type', 'value'] 191 | def withProperties = ['name', new MessageFromPropertyGetter(messageSource, 'type'), 'price.value'] 192 | 193 | new WebXlsxExporter().with { 194 | setResponseHeaders(response) 195 | fillHeader(headers) 196 | add(products, withProperties) 197 | save(response.outputStream) 198 | } 199 | } 200 | ``` 201 | 202 | This will use grails i18n, based on the value of some property (`type` in the example above) of your objects. 203 | 204 | # I want fancy diagrams, colours, and other stuff in my Excel! 205 | 206 | Making xlsx files look really great with Apache POI is pretty fun but not very efficient. So we have found out that it's easier to create a template manually (in MS Excel or Open Office), load this template in your code, fill it up with data, and hand it back to the user. 207 | 208 | For this scenario, every constructor takes a path to a template file (just a normal xlsx file). 209 | 210 | After loading the template, fill the data, and save to the output stream 211 | 212 | ```groovy 213 | new WebXlsxExporter('/tmp/myTemplate.xlsx').with { 214 | setResponseHeaders(response) 215 | add(products, withProperties) 216 | save(response.outputStream) 217 | } 218 | ``` 219 | 220 | If you just want to save the file to disk instead of a stream, use a different constructor: 221 | 222 | ```groovy 223 | new XlsxExporter('/tmp/myTemplate.xlsx', '/tmp/myReport.xlsx') 224 | ``` 225 | 226 | If you just open an existing file, and save it, like this: 227 | 228 | ```groovy 229 | new XlsxExporter('/tmp/myReport.xlsx').with { 230 | add(products, withProperties) 231 | save() 232 | } 233 | ``` 234 | 235 | you are going to overwrite it. 236 | 237 | # But I don't want no template (for whatever reason) 238 | 239 | Ok, so if you don't want to use a temple, but want to format a cell style directly in the code, you can still do that. 240 | 241 | You can get the cell style like this: 242 | 243 | ```groovy 244 | xlsxReporter.getCellAt(0, 0).getCellStyle().getDataFormatString() 245 | ``` 246 | 247 | Of course there is a corresponding `setCellStyle()` method, but this is a part of the [Apache POI API](https://poi.apache.org/apidocs/org/apache/poi/xssf/usermodel/XSSFCell.html), and is outside the scope of this plugin. 248 | 249 | # How to get it installed? 250 | 251 | Like any other Grails plugin, just add to the `dependencies` block of your app's build.gradle file: 252 | ```groovy 253 | dependencies { 254 | compile "org.grails.plugins:excel-export:2.1" 255 | } 256 | ``` 257 | 258 | Excluding xerces may or may not be needed, depending on your setup. If you get 259 | 260 | ``` 261 | Error executing script RunApp: org/apache/xerces/dom/DeferredElementImpl (Use --stacktrace to see the full trace) 262 | ``` 263 | 264 | you NEED to exclude xercesImpl to use ApachePOI. Don't worry, it won't break anything. 265 | 266 | If you have more strange problems with xml and you are using Java 7, exclude xml-apis as well. 267 | 268 | To understand why you need to exclude anything, please take a look here: http://stackoverflow.com/questions/11677572/dealing-with-xerces-hell-in-java-maven 269 | 270 | If you want a working example, clone this project: https://github.com/TouK/excel-export-samples 271 | 272 | # Alternative plugins and solutions 273 | 274 | As noted above, James Kleeh's [groovy-excel-builder](https://github.com/jameskleeh/groovy-excel-builder) provides a wonderful Groovy DSL with varying degress of fine-grained control: 275 | 276 | ```groovy 277 | XSSFWorkbook workbook = ExcelBuilder.build { 278 | sheet { 279 | row { 280 | cell("Test 1") 281 | cell("Test 2") 282 | } 283 | } 284 | } 285 | 286 | workbook.write(outputStream) 287 | ``` 288 | 289 | There are others, but most were too simplistic or too 'automagical' for my needs. Apache POI is pretty simple to use itself (and has fantastic API) but we needed something even simpler for several projects. Also a bit DSL-like so our customers could write reports on their own. After preparing a few getters for our custom objects, this is what we ended up with: 290 | 291 | ```groovy 292 | def withProperties = ["id", "name", "inProduction", "workLogCount", "createdAt", "createdAtDate", asDate("firstEventTime"), 293 | firstUnacknowledgedTime(), firstUnacknowledged(), firstTakeOwnershipTime(), firstTakeOwnership(), 294 | firstReleaseOwnershipTime(), firstReleaseOwnership(), firstClearedTime(), firstCleared(), 295 | firstDeferedTime(), firstDefered(), firstUndeferedTime(), firstUndefered(), childConnectedTime(), childConnected(), 296 | parentConnectedTime(), parentConnected(), parentDisconnectedTime(), parentDisconnected(), 297 | childDisconnectedTime(), childDisconnected(), childOrphanedTime(), childOrphaned(), createdTime(), created(), 298 | updatedTime(), updated(), workLogAddedTime(), workLogAdded()] 299 | 300 | def reporter = new XlsxReporter("/tmp/sampleTemplate.xlsx") 301 | 302 | reporter.with { 303 | fillHeader withProperties 304 | add events, withProperties 305 | save "/tmp/sampleReport1.xlsx" 306 | } 307 | ``` 308 | 309 | All the methods in `withProperties` are static imports generating a new instance of a corresponding `PropertyGetter` implementation. To our surprise, this worked really well with some clients, who started writing their own reports instead of paying us for doing the boring work. 310 | 311 | Hope it helps. 312 | 313 | # License 314 | 315 | Copyright 2012-2014 TouK 316 | 317 | Licensed under the Apache License, Version 2.0 (the "License"); 318 | you may not use this file except in compliance with the License. 319 | You may obtain a copy of the License at 320 | 321 | http://www.apache.org/licenses/LICENSE-2.0 322 | 323 | Unless required by applicable law or agreed to in writing, software 324 | distributed under the License is distributed on an "AS IS" BASIS, 325 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 326 | See the License for the specific language governing permissions and 327 | limitations under the License. 328 | 329 | # Changes 330 | 331 | 2.0.1 upgrade poi to 3.12 (thanks to Sergio Maria Matone) and Grails to 3+ (thanks to mansiarora) 332 | 333 | 0.2.1 calling toString() on unhandled property types, instead of throwing IllegalArgumentException 334 | 335 | 0.2.0 working with multiple sheets and renaming default sheet 336 | 337 | 0.1.10 not exporting release plugin dependency anymore (Issue #14) 338 | 339 | 0.1.9 upgrade to release plugin 3.0.1 (run 'grails refresh-dependencies' if you have problems in grails 2.3.2) 340 | 341 | 0.1.8: fix for grails 2.3.1 (groovy changing how Mixins see private methods) 342 | 343 | 0.1.7: fixed Property Type Validation not accepting subclasses 344 | 345 | 0.1.6: handling maps in object properties 346 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | grailsVersion = project.grailsVersion 4 | } 5 | repositories { 6 | mavenLocal() 7 | maven { url "https://repo.grails.org/grails/core" } 8 | } 9 | dependencies { 10 | classpath "org.grails:grails-gradle-plugin:$grailsVersion" 11 | } 12 | } 13 | 14 | version "2.1" 15 | group "org.grails.plugins" 16 | 17 | apply plugin: "eclipse" 18 | apply plugin: "idea" 19 | apply plugin: "org.grails.grails-plugin" 20 | apply plugin: "org.grails.grails-plugin-publish" 21 | 22 | ext { 23 | grailsVersion = project.grailsVersion 24 | gradleWrapperVersion = project.gradleWrapperVersion 25 | } 26 | 27 | sourceCompatibility = 1.7 28 | targetCompatibility = 1.7 29 | 30 | repositories { 31 | mavenLocal() 32 | maven { url "https://repo.grails.org/grails/core" } 33 | } 34 | 35 | dependencyManagement { 36 | imports { 37 | mavenBom "org.grails:grails-bom:$grailsVersion" 38 | } 39 | applyMavenExclusions false 40 | } 41 | 42 | dependencies { 43 | compile "org.springframework.boot:spring-boot-starter-logging" 44 | compile "org.springframework.boot:spring-boot-autoconfigure" 45 | compile "org.grails:grails-core" 46 | compile "org.springframework.boot:spring-boot-starter-actuator" 47 | compile "org.springframework.boot:spring-boot-starter-tomcat" 48 | compile "org.grails:grails-dependencies" 49 | compile "org.grails:grails-web-boot" 50 | console "org.grails:grails-console" 51 | profile "org.grails.profiles:web-plugin" 52 | testCompile "org.grails:grails-plugin-testing" 53 | 54 | compile('org.apache.poi:poi:3.12') 55 | 56 | compile('org.apache.poi:poi-ooxml:3.12') { 57 | exclude module: 'stax-api' 58 | } 59 | 60 | compile('org.apache.poi:ooxml-schemas:1.1') { 61 | exclude module: 'stax-api' 62 | } 63 | compile('dom4j:dom4j:1.6.1') 64 | 65 | compile('org.apache.commons:commons-io:1.3.2') 66 | compile('xml-apis:xml-apis:1.4.01') 67 | 68 | testRuntime('xerces:xercesImpl:2.11.0') { 69 | exclude module: 'xml-apis' 70 | } 71 | } 72 | 73 | grailsPublish { 74 | userOrg = 'grails-excel-export' 75 | githubSlug = 'TouK/excel-export' 76 | license { 77 | name = 'Apache-2.0' 78 | } 79 | title = "Excel Export Plugin" 80 | desc = "This plugin helps you export data in Excel (xlsx) format, using Apache POI." 81 | developers = [jakubnabrdalik: "Jakub Nabrdalik", mansiarora: "Mansi Arora"] 82 | } 83 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | grailsVersion=3.1.14 2 | gradleWrapperVersion=2.13 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TouK/excel-export/b9179acc828dfb5720261680158625fbde3b92c3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 25 12:38:02 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web-plugin 4 | codegen: 5 | defaultPackage: pl.touk.excel.export 6 | info: 7 | app: 8 | name: '@info.app.name@' 9 | version: '@info.app.version@' 10 | grailsVersion: '@info.app.grailsVersion@' 11 | spring: 12 | groovy: 13 | template: 14 | check-template-location: false 15 | 16 | --- 17 | grails: 18 | mime: 19 | disable: 20 | accept: 21 | header: 22 | userAgents: 23 | - Gecko 24 | - WebKit 25 | - Presto 26 | - Trident 27 | types: 28 | all: '*/*' 29 | atom: application/atom+xml 30 | css: text/css 31 | csv: text/csv 32 | form: application/x-www-form-urlencoded 33 | html: 34 | - text/html 35 | - application/xhtml+xml 36 | js: text/javascript 37 | json: 38 | - application/json 39 | - text/json 40 | multipartForm: multipart/form-data 41 | rss: application/rss+xml 42 | text: text/plain 43 | hal: 44 | - application/hal+json 45 | - application/hal+xml 46 | xml: 47 | - text/xml 48 | - application/xml 49 | urlmapping: 50 | cache: 51 | maxsize: 1000 52 | controllers: 53 | defaultScope: singleton 54 | converters: 55 | encoding: UTF-8 56 | views: 57 | default: 58 | codec: html 59 | gsp: 60 | encoding: UTF-8 61 | htmlcodec: xml 62 | codecs: 63 | expression: html 64 | scriptlets: html 65 | taglib: none 66 | staticparts: none 67 | hibernate: 68 | cache: 69 | queries: false 70 | --- 71 | dataSource: 72 | pooled: true 73 | jmxExport: true 74 | driverClassName: org.h2.Driver 75 | username: sa 76 | password: 77 | 78 | environments: 79 | development: 80 | dataSource: 81 | dbCreate: create-drop 82 | url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 83 | test: 84 | dataSource: 85 | dbCreate: update 86 | url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 87 | production: 88 | dataSource: 89 | dbCreate: update 90 | url: jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 91 | properties: 92 | jmxEnabled: true 93 | initialSize: 5 94 | maxActive: 50 95 | minIdle: 5 96 | maxIdle: 25 97 | maxWait: 10000 98 | maxAge: 600000 99 | timeBetweenEvictionRunsMillis: 5000 100 | minEvictableIdleTimeMillis: 60000 101 | validationQuery: SELECT 1 102 | validationQueryTimeout: 3 103 | validationInterval: 15000 104 | testOnBorrow: true 105 | testWhileIdle: true 106 | testOnReturn: false 107 | jdbcInterceptors: ConnectionState 108 | defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED 109 | -------------------------------------------------------------------------------- /grails-app/conf/logback.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.BuildSettings 2 | import grails.util.Environment 3 | 4 | 5 | // See http://logback.qos.ch/manual/groovy.html for details on configuration 6 | appender('STDOUT', ConsoleAppender) { 7 | encoder(PatternLayoutEncoder) { 8 | pattern = "%level %logger - %msg%n" 9 | } 10 | } 11 | 12 | root(ERROR, ['STDOUT']) 13 | 14 | if(Environment.current == Environment.DEVELOPMENT) { 15 | def targetDir = BuildSettings.TARGET_DIR 16 | if(targetDir) { 17 | 18 | appender("FULL_STACKTRACE", FileAppender) { 19 | 20 | file = "${targetDir}/stacktrace.log" 21 | append = true 22 | encoder(PatternLayoutEncoder) { 23 | pattern = "%level %logger - %msg%n" 24 | } 25 | } 26 | logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /grails-app/init/pl/touk/excel/export/Application.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | 6 | class Application extends GrailsAutoConfiguration { 7 | static void main(String[] args) { 8 | GrailsApp.run(Application, args) 9 | } 10 | } -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/ExcelExportGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import grails.plugins.* 4 | 5 | class ExcelExportGrailsPlugin extends Plugin { 6 | // the version or versions of Grails the plugin is designed for 7 | def grailsVersion = "3.0.2 > *" 8 | 9 | def title = "Excel Export Plugin" // Headline display name of the plugin 10 | def author = "Jakub Nabrdalik" 11 | def authorEmail = "jakubn@gmail.com" 12 | def description = 'This plugin helps you export data in Excel (xlsx) format, using Apache POI.' 13 | 14 | // URL to the plugin's documentation 15 | def documentation = "https://github.com/TouK/excel-export/blob/master/README.md" 16 | 17 | // Extra (optional) plugin metadata 18 | 19 | // License: one of 'APACHE', 'GPL2', 'GPL3' 20 | def license = "APACHE" 21 | 22 | // Details of company behind the plugin (if there is one) 23 | def organization = [name: "TouK", url: "http://touk.pl/"] 24 | 25 | // Any additional developers beyond the author specified above. 26 | def developers = [[name: "Jakub Nabrdalik", email: "jakubn@gmail.com"], [name: "Mansi Arora", email: "mansi.arora@tothenew.com"]] 27 | 28 | // Location of the plugin's issue tracker. 29 | def issueManagement = [system: "Github", url: "https://github.com/TouK/excel-export/issues"] 30 | 31 | // Online location of the plugin's browseable source code. 32 | def scm = [url: "https://github.com/TouK/excel-export"] 33 | 34 | def profiles = ['web'] 35 | 36 | Closure doWithSpring() { {-> 37 | // TODO Implement runtime spring config (optional) 38 | } 39 | } 40 | 41 | void doWithDynamicMethods() { 42 | // TODO Implement registering dynamic methods to classes (optional) 43 | } 44 | 45 | void doWithApplicationContext() { 46 | // TODO Implement post initialization spring config (optional) 47 | } 48 | 49 | void onChange(Map event) { 50 | // TODO Implement code that is executed when any artefact that this plugin is 51 | // watching is modified and reloaded. The event contains: event.source, 52 | // event.application, event.manager, event.ctx, and event.plugin. 53 | } 54 | 55 | void onConfigChange(Map event) { 56 | // TODO Implement code that is executed when the project configuration changes. 57 | // The event is the same as for 'onChange'. 58 | } 59 | 60 | void onShutdown(Map event) { 61 | // TODO Implement code that is executed when the application shuts down (optional) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/Formatters.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import pl.touk.excel.export.getters.AsIsPropertyGetter 4 | import pl.touk.excel.export.getters.Getter 5 | import pl.touk.excel.export.getters.LongToDatePropertyGetter 6 | import pl.touk.excel.export.getters.PropertyGetter 7 | 8 | class Formatters { 9 | static PropertyGetter asDate(String propertyName) { 10 | return new LongToDatePropertyGetter(propertyName) 11 | } 12 | 13 | static PropertyGetter asIs(String propertyName) { 14 | return new AsIsPropertyGetter(propertyName) 15 | } 16 | 17 | static List convertSafelyToGetters(List properties) { 18 | properties.collect { 19 | if (it instanceof Getter) { 20 | return it 21 | } else if(it instanceof String) { 22 | return asIs(it) 23 | } else { 24 | throw IllegalArgumentException('List of properties, which should be either String, a Getter. Found: ' + 25 | it?.toString() + ' of class ' + it?.getClass()) 26 | } 27 | } 28 | } 29 | 30 | static List convertSafelyFromGetters(List properties) { 31 | properties.collect { 32 | (it instanceof Getter) ? it.getPropertyName() : it 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/WebXlsxExporter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import javax.servlet.http.HttpServletResponse 4 | 5 | class WebXlsxExporter extends XlsxExporter { 6 | 7 | WebXlsxExporter() { 8 | super() 9 | } 10 | 11 | WebXlsxExporter(String templateFileNameWithPath) { 12 | File tmpFile = File.createTempFile('tmpWebXlsx', filenameSuffix) 13 | this.fileNameWithPath = tmpFile.getAbsolutePath() 14 | this.workbook = copyAndLoad(templateFileNameWithPath, fileNameWithPath) 15 | setUp(workbook) 16 | } 17 | 18 | WebXlsxExporter setResponseHeaders(HttpServletResponse response) { 19 | setHeaders(response, new Date().format('yyyy-MM-dd_hh-mm-ss') + filenameSuffix) 20 | } 21 | 22 | WebXlsxExporter setResponseHeaders(HttpServletResponse response, Closure filenameClosure) { 23 | setHeaders(response, filenameClosure) 24 | } 25 | 26 | WebXlsxExporter setResponseHeaders(HttpServletResponse response, String filename) { 27 | setHeaders(response, filename) 28 | } 29 | 30 | private WebXlsxExporter setHeaders(HttpServletResponse response, def filename) { 31 | response.setHeader("Content-Disposition", "attachment; filename=\"$filename\"") 32 | response.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") 33 | this 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/XlsxExporter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import groovy.transform.PackageScope 4 | import groovy.transform.TypeChecked 5 | import org.apache.poi.openxml4j.opc.OPCPackage 6 | import org.apache.poi.ss.usermodel.CellStyle 7 | import org.apache.poi.ss.usermodel.CreationHelper 8 | import org.apache.poi.ss.usermodel.DataFormat 9 | import org.apache.poi.ss.usermodel.Sheet 10 | import org.apache.poi.ss.usermodel.Workbook 11 | import org.apache.poi.xssf.usermodel.XSSFWorkbook 12 | import pl.touk.excel.export.abilities.CellManipulationAbility 13 | import pl.touk.excel.export.abilities.FileManipulationAbility 14 | import pl.touk.excel.export.abilities.RowManipulationAbility 15 | import pl.touk.excel.export.multisheet.AdditionalSheet 16 | import pl.touk.excel.export.multisheet.SheetManipulator 17 | 18 | @Mixin([RowManipulationAbility, CellManipulationAbility, FileManipulationAbility]) 19 | @TypeChecked 20 | class XlsxExporter implements SheetManipulator { 21 | static final String defaultSheetName = "Report" 22 | static final String filenameSuffix = ".xlsx" 23 | @PackageScope static final String defaultDateFormat = "yyyy/mm/dd h:mm:ss" 24 | 25 | private Map sheets = [:] 26 | private String worksheetName 27 | private CellStyle dateCellStyle 28 | private CreationHelper creationHelper 29 | protected Workbook workbook 30 | protected String fileNameWithPath 31 | protected OPCPackage zipPackage 32 | 33 | XlsxExporter() { 34 | this.workbook = new XSSFWorkbook() 35 | setUp(workbook) 36 | } 37 | 38 | XlsxExporter(String destinationFileNameWithPath) { 39 | this.fileNameWithPath = destinationFileNameWithPath 40 | this.workbook = createOrLoadWorkbook(destinationFileNameWithPath) 41 | setUp(workbook) 42 | } 43 | 44 | private Workbook createOrLoadWorkbook(String fileNameWithPath) { 45 | if(new File(fileNameWithPath).exists()) { 46 | zipPackage = OPCPackage.open(fileNameWithPath) 47 | return new XSSFWorkbook(zipPackage) 48 | } else { 49 | return new XSSFWorkbook() 50 | } 51 | } 52 | 53 | XlsxExporter(String templateFileNameWithPath, String destinationFileNameWithPath) { 54 | this.fileNameWithPath = destinationFileNameWithPath 55 | this.workbook = copyAndLoad(templateFileNameWithPath, destinationFileNameWithPath) 56 | setUp(workbook) 57 | } 58 | 59 | protected Workbook copyAndLoad(String templateNameWithPath, String destinationNameWithPath) { 60 | if(!new File(templateNameWithPath).exists()) { 61 | throw new IOException("No template file under path: " + templateNameWithPath) 62 | } 63 | copy(templateNameWithPath, destinationNameWithPath) 64 | zipPackage = OPCPackage.open(destinationNameWithPath) 65 | return new XSSFWorkbook(zipPackage) 66 | } 67 | 68 | protected setUp(Workbook workbook) { 69 | this.creationHelper = workbook.getCreationHelper() 70 | this.dateCellStyle = createDateCellStyle(defaultDateFormat) 71 | } 72 | 73 | Sheet getSheet() { 74 | if(sheets.isEmpty()) { 75 | AdditionalSheet additionalSheet = withDefaultSheet() 76 | sheets.put(worksheetName, additionalSheet) 77 | } 78 | return sheets[worksheetName].sheet 79 | } 80 | 81 | AdditionalSheet withDefaultSheet() { 82 | worksheetName = worksheetName ?: defaultSheetName 83 | return sheet(worksheetName) 84 | } 85 | 86 | AdditionalSheet sheet(String sheetName) { 87 | if ( !sheets[sheetName] ) { 88 | Sheet workbookSheet = workbook.getSheet( sheetName ) ?: workbook.createSheet( sheetName ) 89 | sheets[sheetName] = new AdditionalSheet(workbookSheet, workbook.creationHelper, dateCellStyle) 90 | } 91 | return sheets[sheetName] 92 | } 93 | 94 | private void copy(String templateNameWithPath, String destinationNameWithPath) { 95 | zipPackage = OPCPackage.open(templateNameWithPath) 96 | Workbook originalWorkbook = new XSSFWorkbook(zipPackage) 97 | new FileOutputStream(destinationNameWithPath).with { 98 | originalWorkbook.write(it) 99 | } 100 | } 101 | 102 | XlsxExporter setDateCellFormat(String format) { 103 | this.dateCellStyle = createDateCellStyle(format) 104 | return this 105 | } 106 | 107 | private CellStyle createDateCellStyle(String expectedDateFormat) { 108 | CellStyle dateCellStyle = workbook.createCellStyle() 109 | DataFormat dateFormat = workbook.createDataFormat() 110 | dateCellStyle.dataFormat = dateFormat.getFormat(expectedDateFormat) 111 | return dateCellStyle 112 | } 113 | 114 | void setWorksheetName(String worksheetName) { 115 | this.worksheetName = worksheetName 116 | } 117 | 118 | Workbook getWorkbook() { 119 | return workbook 120 | } 121 | 122 | CellStyle getDateCellStyle() { 123 | return dateCellStyle 124 | } 125 | 126 | CreationHelper getCreationHelper() { 127 | return creationHelper 128 | } 129 | 130 | //FIXME: nope, that doesn't work 131 | void finalize() { 132 | closeZipPackageIfPossible() 133 | } 134 | 135 | private void closeZipPackageIfPossible() { 136 | if(zipPackage) { 137 | try { 138 | zipPackage.close() 139 | } finally { 140 | zipPackage = null 141 | } 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/abilities/CellManipulationAbility.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.abilities 2 | 3 | import org.apache.poi.ss.usermodel.Cell 4 | import org.apache.poi.ss.usermodel.Row 5 | import org.apache.poi.ss.usermodel.Sheet 6 | import org.apache.poi.ss.util.CellUtil 7 | import pl.touk.excel.export.getters.Getter 8 | import pl.touk.excel.export.multisheet.SheetManipulator 9 | 10 | @Category(SheetManipulator) 11 | class CellManipulationAbility { 12 | Cell getCellAt(int rowNumber, int columnNumber) { 13 | Row row = CellManipulationAbility.getOrCreateRow(rowNumber, sheet) 14 | row.getCell((Short) columnNumber) 15 | } 16 | 17 | SheetManipulator putCellValue(int rowNumber, int columnNumber, String value) { 18 | CellManipulationAbility.getOrCreateCellAt(rowNumber, columnNumber, sheet).setCellValue(getCreationHelper().createRichTextString(value)) 19 | return this 20 | } 21 | 22 | SheetManipulator putCellValue(int rowNumber, int columnNumber, Getter formatter) { 23 | CellManipulationAbility.putCellValue(rowNumber, columnNumber, formatter.propertyName) 24 | return this 25 | } 26 | 27 | SheetManipulator putCellValue(int rowNumber, int columnNumber, Number value) { 28 | CellManipulationAbility.getOrCreateCellAt(rowNumber, columnNumber, sheet).setCellValue(value.toDouble()) 29 | return this 30 | } 31 | 32 | SheetManipulator putCellValue(int rowNumber, int columnNumber, Date value) { 33 | Cell cell = CellManipulationAbility.getOrCreateCellAt(rowNumber, columnNumber, sheet) 34 | cell.setCellValue(value) 35 | cell.setCellStyle(dateCellStyle) 36 | return this 37 | } 38 | 39 | SheetManipulator putCellValue(int rowNumber, int columnNumber, Boolean value) { 40 | CellManipulationAbility.getOrCreateCellAt(rowNumber, columnNumber, sheet).setCellValue(value) 41 | return this 42 | } 43 | 44 | private static Cell getOrCreateCellAt(int rowNumber, int columnNumber, Sheet sheet) { 45 | CellUtil.getCell(getOrCreateRow(rowNumber, sheet), columnNumber) 46 | } 47 | 48 | private static Row getOrCreateRow(int rowNumber, Sheet sheet) { 49 | CellUtil.getRow(rowNumber, sheet) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/abilities/FileManipulationAbility.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.abilities 2 | 3 | import pl.touk.excel.export.XlsxExporter 4 | 5 | @Category(XlsxExporter) 6 | class FileManipulationAbility { 7 | void save(OutputStream outputStream) { 8 | workbook.write(outputStream) 9 | outputStream.flush() 10 | closeZipPackageIfPossible() 11 | } 12 | 13 | void save() { 14 | if(fileNameWithPath == null) { 15 | throw new Exception("No filename given. You cannot create and save a report without giving filename or OutputStream") 16 | } 17 | deleteIfAlreadyExists() 18 | new FileOutputStream(fileNameWithPath).with { 19 | workbook.write(it) 20 | } 21 | closeZipPackageIfPossible() 22 | } 23 | 24 | void deleteIfAlreadyExists() { 25 | File existingFile = new File(fileNameWithPath) 26 | if (existingFile.exists()) { 27 | existingFile.delete() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/abilities/RowManipulationAbility.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.abilities 2 | 3 | import org.codehaus.groovy.runtime.NullObject 4 | import pl.touk.excel.export.Formatters 5 | import pl.touk.excel.export.getters.Getter 6 | import pl.touk.excel.export.multisheet.SheetManipulator 7 | 8 | import java.sql.Timestamp 9 | 10 | @Category(SheetManipulator) 11 | class RowManipulationAbility { 12 | private static final handledPropertyTypes = [String, Getter, Date, Boolean, Timestamp, NullObject, Long, Integer, BigDecimal, BigInteger, Byte, Double, Float, Short] 13 | 14 | SheetManipulator fillHeader(List properties) { 15 | fillRow(Formatters.convertSafelyFromGetters(properties), 0) 16 | } 17 | 18 | SheetManipulator fillRow(List properties) { 19 | fillRow(properties, 1) 20 | } 21 | 22 | SheetManipulator fillRow(List properties, int rowNumber) { 23 | fillRowWithValues(properties, rowNumber) 24 | } 25 | 26 | SheetManipulator fillRowWithValues(List properties, int rowNumber) { 27 | properties.eachWithIndex { Object property, int index -> 28 | def propertyToBeInserted = RowManipulationAbility.getPropertyToBeInserted(property) 29 | CellManipulationAbility.putCellValue(this, rowNumber, index, propertyToBeInserted) 30 | } 31 | return this 32 | } 33 | 34 | SheetManipulator add(List objects, List selectedProperties) { 35 | add(objects, selectedProperties, 1) 36 | } 37 | 38 | SheetManipulator add(List objects, List selectedProperties, int rowNumber) { 39 | objects.eachWithIndex() { Object object, int index -> 40 | RowManipulationAbility.add(this, object, selectedProperties, rowNumber + index) 41 | } 42 | return this 43 | } 44 | 45 | SheetManipulator add(Object object, List selectedProperties, int rowNumber) { 46 | List properties = RowManipulationAbility.getPropertiesFromObject(object, Formatters.convertSafelyToGetters(selectedProperties)) 47 | fillRow(properties, rowNumber) 48 | } 49 | 50 | private static Object getPropertyToBeInserted(Object property){ 51 | property = property == null ? "" : property 52 | if(!RowManipulationAbility.verifyPropertyTypeCanBeHandled(property)){ 53 | property = property.toString() 54 | } 55 | return property 56 | } 57 | 58 | private static List getPropertiesFromObject(Object object, List selectedProperties) { 59 | selectedProperties.collect { it.getFormattedValue(object) } 60 | } 61 | 62 | private static boolean verifyPropertyTypeCanBeHandled(Object property) { 63 | if(!(handledPropertyTypes.find {it.isAssignableFrom(property.getClass())} )) { 64 | return false 65 | } else { 66 | return true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/getters/AsIsPropertyGetter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.getters 2 | 3 | import groovy.transform.InheritConstructors 4 | 5 | @InheritConstructors 6 | class AsIsPropertyGetter extends PropertyGetter { 7 | 8 | protected format(Object value) { 9 | return value 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/getters/Getter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.getters 2 | 3 | interface Getter { 4 | String getPropertyName() 5 | DestinationFormat getFormattedValue(Object object) 6 | } 7 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/getters/LongToDatePropertyGetter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.getters 2 | 3 | import groovy.transform.InheritConstructors 4 | 5 | @InheritConstructors 6 | class LongToDatePropertyGetter extends PropertyGetter { 7 | 8 | Date format(Long timestamp) { 9 | return new Date(timestamp) 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/getters/MessageFromPropertyGetter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.getters 2 | 3 | import org.springframework.context.MessageSource 4 | import org.springframework.context.i18n.LocaleContextHolder 5 | 6 | class MessageFromPropertyGetter implements Getter { 7 | private MessageSource messageSource 8 | private String propertyName 9 | private Locale locale 10 | 11 | MessageFromPropertyGetter(MessageSource messageSource, String propertyName) { 12 | this.messageSource = messageSource 13 | this.propertyName = propertyName 14 | this.locale = LocaleContextHolder.getLocale() 15 | } 16 | 17 | MessageFromPropertyGetter(MessageSource messageSource, String propertyName, Locale locale) { 18 | this.messageSource = messageSource 19 | this.propertyName = propertyName 20 | this.locale = locale 21 | } 22 | 23 | String getPropertyName() { 24 | return propertyName 25 | } 26 | 27 | Object getFormattedValue(Object object) { 28 | return messageSource.getMessage(object.getProperties().get(propertyName), [].toArray(), object.getProperties().get(propertyName), locale) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/getters/PropertyGetter.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.getters 2 | 3 | abstract class PropertyGetter implements Getter { 4 | protected String propertyName 5 | 6 | PropertyGetter(String propertyName) { 7 | this.propertyName = propertyName 8 | } 9 | 10 | String getPropertyName() { 11 | return propertyName 12 | } 13 | 14 | To getFormattedValue(Object object) { 15 | if(propertyName == null) { 16 | return null 17 | } 18 | if(propertyName.contains('.')) { 19 | return getValueFromChildren(object) 20 | } 21 | return getFormattedPropertyValue(object) 22 | } 23 | 24 | private Object getFormattedPropertyValue(object) { 25 | if ( object instanceof Map ) { 26 | return format( object[ propertyName ] ) 27 | } else { 28 | if (!object.hasProperty(propertyName)) { 29 | return null 30 | } 31 | return format(object."$propertyName") 32 | } 33 | } 34 | 35 | private Object getValueFromChildren(object) { 36 | def value = propertyName.tokenize('.').inject(object) { Object currentObject, propertyName -> 37 | if (!(currentObject instanceof Map) && (currentObject == null || !currentObject.hasProperty(propertyName))) { 38 | return null 39 | } 40 | return currentObject."$propertyName" 41 | } 42 | format(value) 43 | } 44 | 45 | def protected abstract format(From value) 46 | } -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/multisheet/AdditionalSheet.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.multisheet 2 | 3 | import groovy.transform.TypeChecked 4 | import org.apache.poi.ss.usermodel.CellStyle 5 | import org.apache.poi.ss.usermodel.CreationHelper 6 | import org.apache.poi.ss.usermodel.Sheet 7 | import pl.touk.excel.export.abilities.CellManipulationAbility 8 | import pl.touk.excel.export.abilities.RowManipulationAbility 9 | 10 | @Mixin([RowManipulationAbility, CellManipulationAbility]) 11 | @TypeChecked 12 | class AdditionalSheet implements SheetManipulator { 13 | private Sheet sheet 14 | private CreationHelper creationHelper 15 | private CellStyle dateCellStyle 16 | 17 | AdditionalSheet(Sheet sheet, CreationHelper creationHelper, CellStyle dateCellStyle) { 18 | this.sheet = sheet 19 | this.creationHelper = creationHelper 20 | this.dateCellStyle = dateCellStyle 21 | } 22 | 23 | Sheet getSheet() { 24 | return sheet 25 | } 26 | 27 | CreationHelper getCreationHelper() { 28 | return creationHelper 29 | } 30 | 31 | CellStyle getDateCellStyle() { 32 | return dateCellStyle 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/groovy/pl/touk/excel/export/multisheet/SheetManipulator.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.multisheet 2 | 3 | import org.apache.poi.ss.usermodel.CellStyle 4 | import org.apache.poi.ss.usermodel.CreationHelper 5 | import org.apache.poi.ss.usermodel.Sheet 6 | 7 | interface SheetManipulator { 8 | 9 | Sheet getSheet() 10 | 11 | CreationHelper getCreationHelper() 12 | 13 | CellStyle getDateCellStyle() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/SampleObject.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import pl.touk.excel.export.multisheet.AdditionalSheet 4 | 5 | import static pl.touk.excel.export.Formatters.asDate 6 | 7 | class SampleObject { 8 | static final List propertyNames = ["stringValue", "dateValue", "longValue", "booleanValue", asDate("dateAsLong"), "notExistingValue", "child.stringValue", 9 | "bigDecimalValue", "bigIntegerValue", "byteValue", "doubleValue", "floatValue", "integerValue", "shortValue", 10 | "bytePrimitiveValue", "doublePrimitiveValue", "floatPrimitiveValue", "integerPrimitiveValue", "shortPrimitiveValue", "booleanPrimitiveValue", 11 | "simpleMap.simpleMapKey1", "simpleMap.simpleMapKey2", "nestedMap.nestedMapKey.childMapKey", "child", "enumValue"] 12 | 13 | String stringValue = UUID.randomUUID().toString() 14 | Date dateValue = new Date() 15 | Long longValue = 654L 16 | Boolean booleanValue = true 17 | Set setValue = [1, 2, 3, 4] 18 | Long dateAsLong = 1234567890L 19 | ChildObject child = new ChildObject() 20 | BigDecimal bigDecimalValue = BigDecimal.ONE 21 | BigInteger bigIntegerValue = BigInteger.TEN 22 | Byte byteValue = 100 23 | Double doubleValue = 123.45 24 | Float floatValue = 123.45f 25 | Integer integerValue = 99 26 | Short shortValue = 256 27 | byte bytePrimitiveValue = 100 28 | double doublePrimitiveValue = 123.45 29 | float floatPrimitiveValue = 123.45f 30 | int integerPrimitiveValue = 99 31 | short shortPrimitiveValue = 256 32 | boolean booleanPrimitiveValue = 256 33 | def simpleMap = [simpleMapKey1: 'simpleMapValue1', simpleMapKey2: 'simpleMapValue2'] 34 | def nestedMap = [nestedMapKey: [childMapKey: 'childMapValue']] 35 | EnumObject enumValue = EnumObject.SECOND_VALUE 36 | 37 | private void verifyRowHasSelectedProperties(AdditionalSheet additionalSheet, int rowNumber) { 38 | assert additionalSheet.getCellAt(rowNumber, 0)?.stringCellValue == stringValue 39 | assert additionalSheet.getCellAt(rowNumber, 1)?.dateCellValue == dateValue 40 | assert additionalSheet.getCellAt(rowNumber, 2)?.numericCellValue == longValue 41 | assert additionalSheet.getCellAt(rowNumber, 3)?.booleanCellValue == booleanValue 42 | assert additionalSheet.getCellAt(rowNumber, 4)?.dateCellValue == new Date(dateAsLong) 43 | assert additionalSheet.getCellAt(rowNumber, 5)?.stringCellValue == '' 44 | assert additionalSheet.getCellAt(rowNumber, 6)?.stringCellValue == child.stringValue 45 | assert additionalSheet.getCellAt(rowNumber, 7)?.numericCellValue == bigDecimalValue 46 | assert additionalSheet.getCellAt(rowNumber, 8)?.numericCellValue == bigIntegerValue 47 | assert additionalSheet.getCellAt(rowNumber, 9)?.numericCellValue == byteValue 48 | assert additionalSheet.getCellAt(rowNumber, 10)?.numericCellValue == doubleValue 49 | assert additionalSheet.getCellAt(rowNumber, 11)?.numericCellValue == floatValue.toDouble() 50 | assert additionalSheet.getCellAt(rowNumber, 12)?.numericCellValue == integerValue 51 | assert additionalSheet.getCellAt(rowNumber, 13)?.numericCellValue == shortValue 52 | assert additionalSheet.getCellAt(rowNumber, 14)?.numericCellValue == bytePrimitiveValue 53 | assert additionalSheet.getCellAt(rowNumber, 15)?.numericCellValue == doublePrimitiveValue 54 | assert additionalSheet.getCellAt(rowNumber, 16)?.numericCellValue == floatPrimitiveValue.toDouble() 55 | assert additionalSheet.getCellAt(rowNumber, 17)?.numericCellValue == integerPrimitiveValue 56 | assert additionalSheet.getCellAt(rowNumber, 18)?.numericCellValue == shortPrimitiveValue 57 | assert additionalSheet.getCellAt(rowNumber, 19)?.booleanCellValue == booleanPrimitiveValue 58 | assert additionalSheet.getCellAt(rowNumber, 20)?.stringCellValue == simpleMap.simpleMapKey1 59 | assert additionalSheet.getCellAt(rowNumber, 21)?.stringCellValue == simpleMap."simpleMapKey2" 60 | assert additionalSheet.getCellAt(rowNumber, 22)?.stringCellValue == nestedMap.nestedMapKey.childMapKey 61 | assert additionalSheet.getCellAt(rowNumber, 23)?.stringCellValue == child.toString() 62 | assert additionalSheet.getCellAt(rowNumber, 24)?.stringCellValue == EnumObject.SECOND_VALUE.toString() 63 | } 64 | } 65 | 66 | class ChildObject { 67 | String stringValue = "childName" 68 | 69 | String toString(){ 70 | 'String representation of this object' 71 | } 72 | } 73 | 74 | enum EnumObject { 75 | FIRST_VALUE, SECOND_VALUE 76 | } 77 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/SampleObjectWithList.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | class SampleObjectWithList { 4 | List list 5 | 6 | public SampleObjectWithList() { 7 | list = new ArrayList() 8 | list.add(1) 9 | list.add(2) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/WebXlsxExporterSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import org.apache.commons.io.output.NullOutputStream 4 | import spock.lang.Ignore 5 | 6 | class WebXlsxExporterSpec extends XlsxExporterSpec { 7 | String originalValue = "1" 8 | int valueRow = 0 9 | int valueColumn = 0 10 | 11 | /*This test lies. It passes no matter if the real file is overwritten or not. For now, I am not sure why 12 | but it may be for caches. I leave this test here to make sure we do not fall into writing it again and 13 | feeling safe*/ 14 | @Ignore 15 | void "should not overwrite template"() { 16 | given: 17 | new XlsxExporter(getFilePath()).with { 18 | putCellValue(valueRow, valueColumn, originalValue) 19 | save() 20 | } 21 | 22 | when: 23 | new WebXlsxExporter(getFilePath()).with { 24 | putCellValue(0, 0, "2") 25 | save(new NullOutputStream()) 26 | } 27 | 28 | then: 29 | getCellValue(new XlsxExporter(getFilePath()), valueRow, valueColumn) == originalValue 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterCreationSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import groovy.io.GroovyPrintStream 4 | 5 | class XlsxExporterCreationSpec extends XlsxExporterSpec { 6 | 7 | void "should create document if doesn't exist"() { 8 | expect: 9 | !file.exists() 10 | 11 | when: 12 | xlsxReporter.save() 13 | 14 | then: 15 | file.exists() 16 | } 17 | 18 | void "should save to stream"() { 19 | given: 20 | GroovyPrintStream outputStream = new GroovyPrintStream(filePath) 21 | 22 | when: 23 | xlsxReporter.save(outputStream) 24 | 25 | then: 26 | file.exists() 27 | } 28 | 29 | void "should open existing document if exists"() { 30 | given: 31 | String myCustomValue = "myCustomValue" 32 | xlsxReporter.putCellValue(1, 1, myCustomValue) 33 | xlsxReporter.save() 34 | 35 | when: 36 | XlsxExporter newXlsxReporter = new XlsxExporter(filePath) 37 | 38 | then: 39 | getCellValue(newXlsxReporter, 1, 1) == myCustomValue 40 | } 41 | 42 | void "should override existing file"() { 43 | given: 44 | xlsxReporter.putCellValue(1, 1, "old") 45 | xlsxReporter.save() 46 | 47 | when: 48 | XlsxExporter newXlsxReporter = new XlsxExporter(filePath) 49 | newXlsxReporter.putCellValue(1, 1, "new") 50 | newXlsxReporter.save() 51 | 52 | then: "no exceptions are thrown and" 53 | getCellValue(newXlsxReporter, 1, 1) == "new" 54 | } 55 | 56 | void "should not override existing file when giving new path"() { 57 | given: 58 | xlsxReporter.putCellValue(1, 1, "old") 59 | xlsxReporter.save() 60 | 61 | when: 62 | XlsxExporter newXlsxReporter = new XlsxExporter(filePath, testFolder.absolutePath + "/newTestReport.xlsx") 63 | newXlsxReporter.putCellValue(1, 1, "new") 64 | newXlsxReporter.save() 65 | 66 | then: "no exceptions are thrown and" 67 | getCellValue(new XlsxExporter(filePath), 1, 1) == "old" 68 | //otherwise this sucker keeps the value in cache 69 | getCellValue(newXlsxReporter, 1, 1) == "new" 70 | } 71 | 72 | void "should be able to rename initial sheet"() { 73 | given: 74 | String otherSheetName = 'something else' 75 | 76 | when: 77 | XlsxExporter namedSheetExporter = new XlsxExporter() 78 | namedSheetExporter.setWorksheetName(otherSheetName) 79 | 80 | then: 81 | namedSheetExporter.sheet.sheetName == otherSheetName 82 | } 83 | 84 | void "should have default name for initial sheet"() { 85 | expect: 86 | new XlsxExporter().sheet.sheetName == XlsxExporter.defaultSheetName 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterDateStyleSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | class XlsxExporterDateStyleSpec extends XlsxExporterSpec { 4 | 5 | void "should set default date style for date cells"() { 6 | given: 7 | String stringValue = "spomething" 8 | Date dateValue = new Date() 9 | 10 | when: 11 | xlsxReporter. 12 | putCellValue(0, 0, stringValue). 13 | putCellValue(0, 1, dateValue) 14 | 15 | then: 16 | xlsxReporter.getCellAt(0, 0).cellStyle.dataFormatString == "General" 17 | xlsxReporter.getCellAt(0, 1).cellStyle.dataFormatString == XlsxExporter.defaultDateFormat 18 | } 19 | 20 | void "should set date style for date cells"() { 21 | given: 22 | Date dateValue = new Date() 23 | String expectedFormat = "yyyy-MM-dd" 24 | 25 | when: 26 | xlsxReporter.setDateCellFormat(expectedFormat) 27 | xlsxReporter.putCellValue(0, 0, dateValue) 28 | 29 | then: 30 | xlsxReporter.getCellAt(0, 0).cellStyle.dataFormatString == expectedFormat 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterHeaderSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import static pl.touk.excel.export.Formatters.asDate 4 | 5 | class XlsxExporterHeaderSpec extends XlsxExporterSpec { 6 | 7 | void "should create header"() { 8 | given: 9 | List headerProperties = ["First", "Second", "Third"] 10 | 11 | when: 12 | xlsxReporter.fillHeader(headerProperties) 13 | xlsxReporter.save() 14 | 15 | then: 16 | verifyValuesAtRow(headerProperties, 0) 17 | } 18 | 19 | void "should accept dates in header"() { 20 | given: 21 | List headerProperties = ["First", asDate("MyDateAsLong"), "Third"] 22 | 23 | when: 24 | xlsxReporter.fillHeader(headerProperties) 25 | xlsxReporter.save() 26 | 27 | then: 28 | verifyValuesAtRow(headerProperties, 0) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterManipulateCellsSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | class XlsxExporterManipulateCellsSpec extends XlsxExporterSpec { 4 | 5 | void "should put and read cell value"() { 6 | given: 7 | String stringValue = "spomething" 8 | Date dateValue = new Date() 9 | Long longValue = 1L 10 | Boolean booleanValue = true 11 | 12 | when: 13 | xlsxReporter.putCellValue(0, 0, stringValue). 14 | putCellValue(0, 1, dateValue). 15 | putCellValue(0, 2, longValue). 16 | putCellValue(0, 3, booleanValue) 17 | 18 | then: 19 | xlsxReporter.getCellAt(0, 0).stringCellValue == stringValue 20 | xlsxReporter.getCellAt(0, 1).dateCellValue == dateValue 21 | xlsxReporter.getCellAt(0, 2).numericCellValue == longValue 22 | xlsxReporter.getCellAt(0, 3).booleanCellValue == booleanValue 23 | } 24 | 25 | void "should change cell and columns style"() { 26 | given: 27 | String stringValue = """Lorem Ipsum is simply dummy text of the printing and \ 28 | typesetting industry. Lorem Ipsum has been the industry's standard dummy \ 29 | text ever since the 1500s, when an unknown printer took a galley of type \ 30 | and scrambled it to make a type specimen book. It has survived not only five \ 31 | centuries, but also the leap into electronic typesetting, remaining essentially""" 32 | xlsxReporter.putCellValue(0, 0, stringValue) 33 | 34 | when: 35 | xlsxReporter.getCellAt(0, 0).cellStyle.setShrinkToFit(true) 36 | then: 37 | xlsxReporter.getCellAt(0, 0)?.cellStyle?.shrinkToFit 38 | 39 | // column 40 | when: 41 | xlsxReporter.sheet.setColumnWidth(0, 70) 42 | then: 43 | xlsxReporter.sheet.getColumnWidth(0) == 70 44 | 45 | when: 46 | xlsxReporter.sheet.getColumnStyle(0).setShrinkToFit(true) 47 | then: 48 | xlsxReporter.sheet.getColumnStyle(0).shrinkToFit 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterMultipleSheetSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | class XlsxExporterMultipleSheetSpec extends XlsxExporterSpec { 4 | 5 | void "should fill rows in separate sheets"() { 6 | given: 7 | List objectsForDefaultSheet = [new SampleObject(), new SampleObject(), new SampleObject()] 8 | List objectsForNamedSheet = [new SampleObject(), new SampleObject(), new SampleObject()] 9 | String sheetName = "sheet2" 10 | 11 | when: 12 | xlsxReporter.add(objectsForDefaultSheet, SampleObject.propertyNames, 0) 13 | xlsxReporter.sheet(sheetName).add( objectsForNamedSheet, SampleObject.propertyNames, 0) 14 | 15 | then: 16 | verifySaved(objectsForDefaultSheet, XlsxExporter.defaultSheetName) 17 | verifySaved(objectsForNamedSheet, sheetName) 18 | } 19 | 20 | private void verifySaved(ArrayList objects, String sheetName) { 21 | objects.eachWithIndex { SampleObject sampleObject, int i -> 22 | sampleObject.verifyRowHasSelectedProperties(xlsxReporter.sheet(sheetName), i) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterRowSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import org.apache.poi.xssf.usermodel.XSSFWorkbook 4 | 5 | class XlsxExporterRowSpec extends XlsxExporterSpec { 6 | 7 | void "should fill row at first position"() throws IOException { 8 | given: 9 | List rowValues = ["First", "Second", "Third", " "] 10 | 11 | when: 12 | xlsxReporter.fillRow(rowValues) 13 | xlsxReporter.save() 14 | 15 | then: 16 | verifyValuesAtRow(rowValues, 1) 17 | } 18 | 19 | void "should fill row at position"() throws IOException { 20 | given: 21 | List rowValues = ["First", "Second", "Third", " "] 22 | int rowNumber = 15 23 | 24 | when: 25 | xlsxReporter.fillRow(rowValues, rowNumber) 26 | xlsxReporter.save() 27 | 28 | then: 29 | verifyValuesAtRow(rowValues, rowNumber) 30 | } 31 | 32 | void "should fill row at position with zero"() throws IOException { 33 | given: 34 | List rowValues = [0] 35 | int rowNumber = 25 36 | 37 | when: 38 | xlsxReporter.fillRow(rowValues, rowNumber) 39 | xlsxReporter.save() 40 | 41 | then: 42 | getCell(new XSSFWorkbook(filePath), rowNumber, 0).numericCellValue == 0 43 | } 44 | 45 | void "should handle subclasses of valid types in properties"() { 46 | given: 47 | java.util.Date handledType = new java.util.Date(123123) 48 | java.sql.Date subclassOfHandledType = new java.sql.Date(321321) 49 | List rowValues = [handledType, subclassOfHandledType] 50 | 51 | when: 52 | xlsxReporter.fillRow(rowValues) 53 | xlsxReporter.save() 54 | 55 | then: 56 | verifyDateAt(handledType, 1, 0) 57 | verifyDateAt(subclassOfHandledType, 1, 1) 58 | } 59 | 60 | void "should fill row from property list"() { 61 | given: 62 | SampleObject testObject = new SampleObject() 63 | 64 | when: 65 | xlsxReporter.add(testObject, SampleObject.propertyNames, 3) 66 | 67 | then: 68 | testObject.verifyRowHasSelectedProperties(xlsxReporter.withDefaultSheet(), 3) 69 | } 70 | 71 | void "should fill rows"() { 72 | given: 73 | List objects = [new SampleObject(), new SampleObject(), new SampleObject()] 74 | 75 | when: 76 | xlsxReporter.add(objects, SampleObject.propertyNames, 0) 77 | 78 | then: 79 | objects.eachWithIndex { SampleObject sampleObject, int i -> 80 | sampleObject.verifyRowHasSelectedProperties(xlsxReporter.withDefaultSheet(), i) 81 | } 82 | } 83 | 84 | void "should fill rows from lists of maps"() { 85 | given: 86 | List stones = [[first: 'Keith'], [first: 'Mick']] 87 | 88 | when: 89 | xlsxReporter.add(stones, ['first'], 0) 90 | 91 | then: 92 | xlsxReporter.getCellAt(0, 0).stringCellValue == stones[0].first 93 | xlsxReporter.getCellAt(1, 0).stringCellValue == stones[1].first 94 | } 95 | 96 | void "should handle nulls when found in map"() { 97 | given: 98 | List stones = [[middle: null]] 99 | 100 | when: 101 | xlsxReporter.add(stones, ['middle'], 0) 102 | 103 | then: 104 | xlsxReporter.getCellAt(0, 0).stringCellValue == '' 105 | } 106 | 107 | void "should handle list as to string"() { 108 | given: 109 | List objects = [new SampleObjectWithList(), new SampleObjectWithList()] 110 | 111 | when: 112 | xlsxReporter.add(objects, ['list'], 0) 113 | 114 | then: 115 | xlsxReporter.getCellAt(0, 0).stringCellValue == objects[0].list.toString() 116 | xlsxReporter.getCellAt(1, 0).stringCellValue == objects[1].list.toString() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxExporterSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import org.junit.Before 4 | 5 | abstract class XlsxExporterSpec extends XlsxTestOnTemporaryFolder { 6 | XlsxExporter xlsxReporter 7 | 8 | @Before 9 | void setUpReporter() { 10 | xlsxReporter = new XlsxExporter(filePath) 11 | } 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/XlsxTestOnTemporaryFolder.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export 2 | 3 | import org.apache.poi.xssf.usermodel.XSSFCell 4 | import org.apache.poi.xssf.usermodel.XSSFWorkbook 5 | import org.junit.Before 6 | import org.junit.Rule 7 | import org.junit.rules.TemporaryFolder 8 | import pl.touk.excel.export.getters.Getter 9 | import spock.lang.Specification 10 | 11 | abstract class XlsxTestOnTemporaryFolder extends Specification { 12 | @Rule 13 | TemporaryFolder temporaryFolder = new TemporaryFolder() 14 | File testFolder 15 | 16 | @Before 17 | void setUpTestFolder() { 18 | testFolder = temporaryFolder.root 19 | } 20 | 21 | protected String getFilePath() { 22 | return testFolder.absolutePath + "/testReport.xlsx" 23 | } 24 | 25 | protected File getFile() { 26 | new File(filePath) 27 | } 28 | 29 | protected void verifyDateAt(Date date, int rowNumber, int columnNumber) { 30 | XSSFWorkbook workbook = new XSSFWorkbook(filePath) 31 | assert getCell(workbook, rowNumber, columnNumber).getDateCellValue() == date 32 | } 33 | 34 | protected void verifyValuesAtRow(List values, int rowNumber) { 35 | XSSFWorkbook workbook = new XSSFWorkbook(filePath) 36 | values.eachWithIndex { Object value, int index -> 37 | String propertyName = (value instanceof Getter) ? value.propertyName : value 38 | assert getCellValue(workbook, rowNumber, index) == propertyName 39 | } 40 | } 41 | 42 | protected String getCellValue(XlsxExporter xlsxExporter, int rowNumber, int columnNumber) { 43 | return getCellValue(xlsxExporter.workbook, rowNumber, columnNumber) 44 | } 45 | 46 | protected String getCellValue(XSSFWorkbook workbook, int rowNumber, int columnNumber) { 47 | return getCell(workbook, rowNumber, columnNumber).stringCellValue 48 | } 49 | 50 | protected XSSFCell getCell(XSSFWorkbook workbook, int rowNumber, int columnNumber) { 51 | return workbook.getSheet(XlsxExporter.defaultSheetName).getRow(rowNumber).getCell(columnNumber) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/groovy/pl/touk/excel/export/getters/PropertyGetterSpec.groovy: -------------------------------------------------------------------------------- 1 | package pl.touk.excel.export.getters 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | class PropertyGetterSpec extends Specification { 7 | 8 | @Unroll 9 | void "as-is property getter returns an unmodified value of '#raw'"() { 10 | given: 11 | Map object = [asIs: raw] 12 | PropertyGetter getter = new AsIsPropertyGetter('asIs') 13 | 14 | when: 15 | def formatted = getter.getFormattedValue(object) 16 | 17 | then: 18 | formatted == raw 19 | 20 | where: 21 | raw | _ 22 | 0 | _ 23 | "" | _ 24 | new Date() | _ 25 | 1L | _ 26 | true | _ 27 | null | _ 28 | 1.0f | _ 29 | 1.0d | _ 30 | } 31 | 32 | @Unroll 33 | void "long-to-date property getter returns a date value of '#dateVal'"() { 34 | given: 35 | Map object = [raw: longVal] 36 | PropertyGetter getter = new LongToDatePropertyGetter('raw') 37 | 38 | when: 39 | def formatted = getter.getFormattedValue(object) 40 | 41 | then: 42 | formatted == dateVal 43 | 44 | where: 45 | longVal | dateVal 46 | 100l | new Date(100l) 47 | 99999999l | new Date(99999999l) 48 | } 49 | } 50 | --------------------------------------------------------------------------------