source: branches/features/purchaseOrders/grails-app/services/AssetCsvService.groovy @ 963

Last change on this file since 963 was 920, checked in by gav, 14 years ago

Svn merge -r874:r919 trunk/ into branches/features/purchaseOrders.
This brings the purchaseOrder branch fully up to date.

File size: 18.8 KB
Line 
1import grails.util.GrailsUtil
2import au.com.bytecode.opencsv.CSVWriter
3import au.com.bytecode.opencsv.CSVReader
4import org.apache.commons.lang.WordUtils
5
6/**
7 * Provides some csv import/export methods.
8 * Requires the opencsv jar to be available which is included in the grails-export plugin.
9 */
10class AssetCsvService {
11
12    boolean transactional = false
13
14    def g = new org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib()
15
16    def sessionFactory
17    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP
18
19    /**
20    * Import an asset tree creating items as required.
21    * @param request The http request to run getFile against.
22    * Get file should return a csv format file containing the asset tree as per template.
23    */
24    def importAssetTree(request) {
25        Asset.withTransaction { status ->
26            def result = [:]
27
28            def kByteMultiplier = 1000
29            def fileMaxSize = 500 * kByteMultiplier
30            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}
31
32            def multiPartFile = request.getFile('file')
33
34            InputStreamReader sr = new InputStreamReader(multiPartFile.inputStream)
35            CSVReader reader = new CSVReader(sr)
36
37            def fail = { Map m ->
38                status.setRollbackOnly()
39                reader.close()
40                result.error = [ code: m.code, args: m.args ]
41                return result
42            }
43
44            if(!multiPartFile || multiPartFile.isEmpty())
45                return fail(code: "default.file.not.supplied")
46
47            if (multiPartFile.getSize() > fileMaxSize)
48                return fail(code: "default.file.over.max.size", args: [fileMaxSize/kByteMultiplier, "kB"])
49
50            def columnIndex = 0
51            def nameColumnIndex = 0
52            def numberOfColumns = 0
53            def maxNumberOfColumns = 20
54
55            // Get first line.
56            def line = reader.readNext()
57            def lineNumber = 1
58
59            // Check for header line 1.
60            if(line != templateHeaderLine1) {
61                log.error "Failed to find header line 1. "
62                log.error "Required: " + templateHeaderLine1.toString()
63                log.error "Supplied: " + line.toString()
64                return fail(code: "default.file.no.header")
65            }
66
67            // Get second line.
68            line = reader.readNext()
69            lineNumber ++
70
71            // Check for header line 2.
72            if(line != templateHeaderLine2) {
73                log.error "Failed to find header line 2. "
74                log.error "Required: " + templateHeaderLine2.toString()
75                log.error "Supplied: " + line.toString()
76                return fail(code: "default.file.no.header")
77            }
78
79            log.info "Import checks passed, start processing asset file."
80
81            // Prepare the first body line.
82            line = reader.readNext()
83            lineNumber ++
84
85            def siteInstance
86            def departmentInstance
87            def sectionInstance
88            def assetInstance
89            def assetSubItemInstance
90            def parentItem
91
92            def column = [:]
93
94            def nextLine = {
95                    line = reader.readNext()
96                    lineNumber ++
97                    log.info "Processing line: " + lineNumber
98            }
99
100            def nextColumn = {
101                nameColumnIndex = columnIndex
102
103                if( (columnIndex+1) < numberOfColumns ) {
104                    // Capitalise and assign the name and description columns.
105                    use(WordUtils) {
106                    column.name = line[columnIndex].trim().capitalize()
107                    column.description = line[++columnIndex].trim().capitalize()
108                    }
109                }
110                else if( columnIndex < numberOfColumns ) {
111                    // Capitalise and assign the name column with a blank description.
112                    use(WordUtils) {
113                    column.name = line[columnIndex].trim().capitalize()
114                    column.description = ''
115                    }
116                }
117                else {
118                    log.info "No more columns on line: " + lineNumber
119                    return false
120                }
121
122                if(!column.name) {
123                    log.info "No name at " + "line: " + lineNumber + " col: " + nameColumnIndex
124                    return false
125                }
126
127                columnIndex++
128                // Success.
129                return column
130            }
131
132            // Primary loop.
133            while(line) {
134                numberOfColumns = Math.min( line.size(), maxNumberOfColumns )
135                columnIndex = 0
136
137                if(!nextColumn()) {
138                    nextLine()
139                    continue
140                }
141
142                // Ignore comment lines.
143                if(line.toString().toLowerCase().contains("comment")) {
144                    log.info "Comment line found."
145                    nextLine()
146                    continue
147                }
148
149                // Ignore example lines.
150                if(line.toString().toLowerCase().contains("example")) {
151                    log.info "Example line found."
152                    nextLine()
153                    continue
154                }
155
156                // Site.
157                siteInstance = Site.findByName(column.name)
158                if(!siteInstance) {
159                    log.info "Creating site: " + column.name
160                    siteInstance = new Site(name: column.name,
161                                                                description: column.description)
162                    if(!siteInstance.save()) {
163                        log.error "Failed to create site on line: " + column.name + "(" + lineNumber + ")"
164                        return fail(code: "asset.tree.import.failure", args: [lineNumber, logFileLink])
165                    }
166                }
167                else log.info "Existing site: " + siteInstance
168
169                if(!nextColumn()) {
170                    nextLine()
171                    continue
172                }
173
174                // Department.
175                departmentInstance = Department.findByName(column.name)
176                if(!departmentInstance) {
177                    log.info "Creating department: " + column.name
178                    departmentInstance = new Department(name: column.name,
179                                                                                            description: column.description,
180                                                                                            site: siteInstance)
181                    if(!departmentInstance.save()) {
182                        log.error "Failed to create department on line: " + column.name + "(" + lineNumber + ")"
183                        return fail(code: "asset.tree.import.failure", args: [lineNumber, logFileLink])
184                    }
185                }
186                else log.info "Existing department: " + departmentInstance
187
188                if(!nextColumn()) {
189                    nextLine()
190                    continue
191                }
192
193                // Section.
194                sectionInstance = Section.findByName(column.name)
195                if(!sectionInstance) {
196                    log.info "Creating section: " + column.name
197                    sectionInstance =  new Section(name: column.name,
198                                                                            description: column.description,
199                                                                            site: siteInstance,
200                                                                            department: departmentInstance)
201                    if(!sectionInstance.save()) {
202                        log.error "Failed to create section on line: " + column.name + "(" + lineNumber + ")"
203                        return fail(code: "asset.tree.import.failure", args: [lineNumber, logFileLink])
204                    }
205                }
206                else log.info "Existing section: " + sectionInstance
207
208                if(!nextColumn()) {
209                    nextLine()
210                    continue
211                }
212
213                // Asset.
214                assetInstance = Asset.findByName(column.name)
215                if(!assetInstance) {
216                    log.info "Creating asset: " + column.name
217                    assetInstance = new Asset(name: column.name,
218                                                                    description: column.description,
219                                                                    section: sectionInstance)
220                    if(!assetInstance.save()) {
221                        log.error "Failed to create asset on line: " + column.name + "(" + lineNumber + ")"
222                        return fail(code: "asset.tree.import.failure", args: [lineNumber, logFileLink])
223                    }
224                }
225                else log.info "Existing asset: " + assetInstance
226
227                if(!nextColumn()) {
228                    nextLine()
229                    continue
230                }
231
232                // AssetSubItem Level 1.
233                assetSubItemInstance = AssetSubItem.findByName(column.name)
234                if(!assetSubItemInstance) {
235                    log.info "Creating asset sub item: " + column.name
236                    assetSubItemInstance = new AssetSubItem(name: column.name,
237                                                                                                description: column.description)
238                    if(!assetInstance.save()) {
239                        log.error "Failed to create assetSubItem on line: " + column.name + "(" + lineNumber + ")"
240                        return fail(code: "asset.tree.import.failure", args: [lineNumber, logFileLink])
241                    }
242                }
243                else log.info "Existing asset sub item: " + assetSubItemInstance
244
245                assetInstance.addToAssetSubItems(assetSubItemInstance)
246
247                // Remaining AssetSubItems.
248                while( nextColumn() ) {
249
250                    parentItem = assetSubItemInstance
251                    assetSubItemInstance = AssetSubItem.findByName(column.name)
252                    if(!assetSubItemInstance) {
253                        log.info "Creating asset sub item: " + column.name
254                        assetSubItemInstance = new AssetSubItem(name: column.name,
255                                                                                                    description: column.description,
256                                                                                                    parentItem: parentItem)
257                        if(!assetSubItemInstance.save()) {
258                            log.error "Failed to create assetSubItem on line: " + column.name + "(" + lineNumber + ")"
259                            return fail(code: "asset.tree.import.failure", args: [lineNumber, logFileLink])
260                        }
261                    }
262                    else log.info "Existing asset sub item: " + assetSubItemInstance
263
264                } // while( nextColumn() )
265
266                if(lineNumber % 100 == 0)
267                    cleanUpGorm()
268
269                nextLine()
270            } //while(line)
271
272            // Success.
273            log.info "End of file."
274            reader.close()
275            return result
276
277        } //end withTransaction
278    } // end importAssetTree()
279
280    /**
281    * Build an asset tree template csv file.
282    * This template can then be populated for import.
283    * @returns The template as a String in csv format.
284    */
285    def buildAssetTreeTemplate() {
286
287        StringWriter sw = new StringWriter()
288        CSVWriter writer = new CSVWriter(sw)
289
290        writeTemplateLines(writer)
291
292        writer.close()
293        return sw.toString()
294    }
295
296    private writeTemplateLines(writer) {
297        writer.writeNext(templateHeaderLine1 as String[])
298        writer.writeNext(templateHeaderLine2 as String[])
299        writer.writeNext()
300        writer.writeNext(["Example: Site1", "", "Department 1", "", "Section 1", "", "Asset 1", ""] as String[])
301        writer.writeNext()
302        writer.writeNext(["Example: Site1", "", "Department 1", "", "Section 1", "", "Asset 2", ""] as String[])
303        writer.writeNext()
304        writer.writeNext("Comment: The first two header lines are required.")
305        writer.writeNext("Comment: Leave a blank line between assets.")
306        writer.writeNext("Comment: Names are required, descriptions are optional.")
307        writer.writeNext("Comment: Identical and existing names will be considered as the same item.")
308        writer.writeNext("Comment: An asset may share the first level of asset sub items with other assets.")
309        writer.writeNext("Comment: Lower levels of asset sub items are only parented once but may have many children.")
310        writer.writeNext("Comment: Lines containing 'comment' will be ignored.")
311        writer.writeNext("Comment: Lines containing 'example' will be ignored.")
312        writer.writeNext("Comment: This file must be saved as a CSV file before import.")
313        writer.writeNext()
314    }
315
316    /**
317    * Build an asset tree test file.
318    * This test file can be imported to test the import and template methods.
319    * @returns The test file as a String in csv format.
320    */
321    def buildAssetTreeTest() {
322
323        StringWriter sw = new StringWriter()
324        CSVWriter writer = new CSVWriter(sw)
325
326        writeTemplateLines(writer)
327
328        writer.writeNext(["Lake Press", "Lake Press Site",
329                                        "Print Department", "The printing department",
330                                        "Press Section", "Contains all printing units",
331                                        "Print Unit 1", "Printing Unit Number 1",
332                                        "Print Unit", "Print Unit (Common to all units)",
333                                        "Print Couple", "Includes  blanket cylinder",
334                                        "Motor", "Main Drive Motor",
335                                        "NDS Bearing", "Non Drive Side Main Bearing"
336                                        ] as String[])
337
338        writer.writeNext(["Lake Press", "Lake Press Site",
339                                        "Print Department", "The printing department",
340                                        "Press Section", "Contains all printing units",
341                                        "Print Unit 1", "Printing Unit Number 1",
342                                        "Print Unit", "Print Unit (Common to all units)",
343                                        "Print Couple", "Includes  blanket cylinder",
344                                        "Motor", "Main Drive Motor",
345                                        "DS Bearing", "Drive Side Main Bearing"
346                                        ] as String[])
347
348        writer.close()
349        return sw.toString()
350    }
351
352    /**
353    * Build complete asset trees for export.
354    * @param assetList The list of assets to build and export trees for.
355    * @returns The tree as a String in csv format.
356    */
357    def buildAssetTree(List assetList) {
358
359        StringWriter sw = new StringWriter()
360        CSVWriter writer = new CSVWriter(sw)
361
362        writeTemplateLines(writer)
363
364        //Rows
365        def row
366
367        def writeAssetSubItem4 = { assetSubItem ->
368            row.add(assetSubItem.name)
369            row.add(assetSubItem.description)
370            writer.writeNext(row as String[])
371        }
372
373        def writeAssetSubItem3 = { assetSubItem ->
374            row.add(assetSubItem.name)
375            row.add(assetSubItem.description)
376
377            if(assetSubItem.subItems.size() > 0) {
378                assetSubItem.subItems.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { assetSubItem4 ->
379                    writeAssetSubItem4(assetSubItem4)
380                    row.removeRange(row.size()-2, row.size())
381                }
382            }
383            else {
384                writer.writeNext(row as String[])
385            }
386
387        }
388
389        def writeAssetSubItem2 = { assetSubItem ->
390            row.add(assetSubItem.name)
391            row.add(assetSubItem.description)
392
393            if(assetSubItem.subItems.size() > 0) {
394                assetSubItem.subItems.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { assetSubItem3 ->
395                    writeAssetSubItem3(assetSubItem3)
396                    row.removeRange(row.size()-2, row.size())
397                }
398            }
399            else {
400                writer.writeNext(row as String[])
401            }
402
403        }
404
405        def writeAssetSubItem1 = { assetSubItem ->
406            row.add(assetSubItem.name)
407            row.add(assetSubItem.description)
408
409            if(assetSubItem.subItems.size() > 0) {
410                assetSubItem.subItems.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { assetSubItem2 ->
411                    writeAssetSubItem2(assetSubItem2)
412                    row.removeRange(row.size()-2, row.size())
413                }
414            }
415            else {
416                writer.writeNext(row as String[])
417            }
418
419        }
420
421        assetList.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { asset ->
422            row = []
423            writer.writeNext(row as String[]) //blank row between assets.
424            row.add(asset.section.site.name)
425            row.add(asset.section.site.description)
426            row.add(asset.section.department.name)
427            row.add(asset.section.department.description)
428            row.add(asset.section.name)
429            row.add(asset.section.description)
430            row.add(asset.name)
431            row.add(asset.description)
432
433            if(asset.assetSubItems.size() > 0) {
434                asset.assetSubItems.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { assetSubItem1 ->
435                    writeAssetSubItem1(assetSubItem1)
436                    row.removeRange(row.size()-2, row.size())
437                }
438            }
439            else {
440                writer.writeNext(row as String[])
441            }
442
443        }
444
445        writer.close()
446        return sw.toString()
447    } // end buildAssetTree
448
449    private getTemplateHeaderLine1() {
450            ["Site", "", "Department", "", "Section", "","Asset", "", "Sub Asset", "", "Functional Assembly", "", "Sub Assembly Group", "", "SubItem", ""]
451    }
452
453    private getTemplateHeaderLine2() {
454            (["Name", "Description"])*8
455    }
456
457    /**
458    * This cleans up the hibernate session and a grails map.
459    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
460    * The hibernate session flush is normal for hibernate.
461    * The map is apparently used by grails for domain object validation errors.
462    * A starting point for clean up is every 100 objects.
463    */
464    def cleanUpGorm() {
465        def session = sessionFactory.currentSession
466        session.flush()
467        session.clear()
468        propertyInstanceMap.get().clear()
469    }
470
471} // end class
Note: See TracBrowser for help on using the repository browser.