source: trunk/grails-app/services/InventoryItemService.groovy @ 636

Last change on this file since 636 was 636, checked in by gav, 9 years ago

Add feature to import inventory item pictures from zip file, part 2.

File size: 17.9 KB
Line 
1import org.codehaus.groovy.grails.commons.ConfigurationHolder
2import org.apache.commons.lang.WordUtils
3
4/**
5* Provides a service class for the InventoryItem domain class.
6*/
7class InventoryItemService {
8
9    boolean transactional = false
10
11    def createDataService
12
13    def sessionFactory
14    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP
15
16    /**
17    * Prepare a sorted list of possible alternateItems.
18    */
19    def getPossibleAlternateItems(inventoryItemInstance) {
20        def criteria = inventoryItemInstance.createCriteria()
21        def possibleAlternateItems = criteria {
22            and {
23                eq('isActive', true)
24                notEqual('id', inventoryItemInstance.id)
25            }
26        }.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }
27    }
28
29    /**
30    * Prepare the data for the show view.
31    * The result can be used to easily construct the model for the show view.
32    * @param params The incoming params as normally passed to the show view
33    * primarily including the id of the inventoryItem.
34    * @returns A map containing result.error, if any error, otherwise result.inventoryItemInstance.
35    */
36    def show(params) {
37        def result = [:]
38
39        def fail = { Map m ->
40            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
41            return result
42        }
43
44        result.inventoryItemInstance = InventoryItem.get( params.id )
45
46        if(!result.inventoryItemInstance)
47            return fail(code:"default.not.found")
48
49        def p = [:]
50
51        if(params.paginate == "purchases") {
52            params.showTab = "showPurchasingTab"
53            p.max = Math.min(params.max?.toInteger() ?: 10, 100)
54            p.offset = params.offset?.toInteger() ?: 0
55            p.sort = params.sort ?: null
56            p.order = params.order ?: null
57        }
58        else {
59            p.max = 10
60            p.offset = 0
61        }
62
63        result.inventoryItemPurchasesTotal = InventoryItemPurchase.countByInventoryItem(result.inventoryItemInstance)
64
65        result.inventoryItemPurchases = InventoryItemPurchase.withCriteria {
66                eq("inventoryItem", result.inventoryItemInstance)
67                maxResults(p.max)
68                firstResult(p.offset)
69                // Sorting:
70                // Default is to sort by order number then id.
71                // When a sortable column is clicked then we sort by that.
72                // If the sortable column clicked is order number then we add id as the second sort.
73                if(p.sort && p.order) {
74                    order(p.sort, p.order)
75                    if(p.sort == "purchaseOrderNumber") order('id', 'asc')
76                }
77                else {
78                    order('purchaseOrderNumber', 'desc')
79                    order('id', 'asc')
80                }
81            }
82
83        result.showTab = [:]
84        switch (params.showTab) {
85            case "showDetailTab":
86                result.showTab.detail =  new String("true")
87                break
88            case "showMovementTab":
89                result.showTab.movement =  new String("true")
90                break
91            case "showPurchasingTab":
92                result.showTab.purchasing =  new String("true")
93                break
94            default:
95                result.showTab.inventory = new String("true")
96        }
97
98        p.max = result.inventoryMovementListMax = 10
99        p.offset = 0
100        p.order = "desc"
101        p.sort = "id"
102        result.inventoryMovementList = InventoryMovement.findAllByInventoryItem(result.inventoryItemInstance, p)
103        result.inventoryMovementListTotal = InventoryMovement.countByInventoryItem(result.inventoryItemInstance)
104
105
106        // Success.
107        return result
108
109    } // end show()
110
111    def delete(params) {
112        InventoryItem.withTransaction { status ->
113            def result = [:]
114
115            def fail = { Map m ->
116                status.setRollbackOnly()
117                if(result.inventoryItemInstance && m.field)
118                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
119                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
120                return result
121            }
122
123            result.inventoryItemInstance = InventoryItem.get(params.id)
124
125            if(!result.inventoryItemInstance)
126                return fail(code:"default.not.found")
127
128            if(result.inventoryItemInstance.inventoryMovements)
129                return fail(code:"inventoryMovement.still.associated")
130
131            result.inventoryItemInstance.removeReverseAlternateItems()
132
133            try {
134                result.inventoryItemInstance.delete(flush:true)
135                return result //Success.
136            }
137            catch(org.springframework.dao.DataIntegrityViolationException e) {
138                return fail(code:"default.delete.failure")
139            }
140
141        } //end withTransaction
142    } // end delete()
143
144    def edit(params) {
145        def result = [:]
146        def fail = { Map m ->
147            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
148            return result
149        }
150
151        result.inventoryItemInstance = InventoryItem.get(params.id)
152
153        if(!result.inventoryItemInstance)
154            return fail(code:"default.not.found")
155
156        // Success.
157        return result
158    }
159
160    def update(params) {
161        InventoryItem.withTransaction { status ->
162            def result = [:]
163
164            def fail = { Map m ->
165                status.setRollbackOnly()
166                if(result.inventoryItemInstance && m.field)
167                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
168                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
169                return result
170            }
171
172            result.inventoryItemInstance = InventoryItem.get(params.id)
173
174            if(!result.inventoryItemInstance)
175                return fail(code:"default.not.found")
176
177            // Optimistic locking check.
178            if(params.version) {
179                if(result.inventoryItemInstance.version > params.version.toLong())
180                    return fail(field:"version", code:"default.optimistic.locking.failure")
181            }
182
183            def previousAlternateItems = new ArrayList(result.inventoryItemInstance.alternateItems)
184
185            result.inventoryItemInstance.properties = params
186
187            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
188                return fail(code:"default.update.failure")
189
190            result.inventoryItemInstance.removeReverseAlternateItems(previousAlternateItems)
191            result.inventoryItemInstance.addReverseAlternateItems()
192
193            // Success.
194            return result
195
196        } //end withTransaction
197    }  // end update()
198
199    def create(params) {
200        def result = [:]
201        def fail = { Map m ->
202            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
203            return result
204        }
205
206        result.inventoryItemInstance = new InventoryItem()
207        result.inventoryItemInstance.properties = params
208
209        // success
210        return result
211    }
212
213    def save(params) {
214        InventoryItem.withTransaction { status ->
215            def result = [:]
216
217            def fail = { Map m ->
218                status.setRollbackOnly()
219                if(result.inventoryItemInstance && m.field)
220                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
221                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
222                return result
223            }
224
225            result.inventoryItemInstance = new InventoryItem(params)
226
227            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
228                return fail(code:"default.create.failure")
229
230            // success
231            return result
232
233        } //end withTransaction
234    }
235
236    /**
237    * Save an inventory item picture.
238    * @param params An object or map containing at least the inventoryItem ID.
239    * @param pictureSource A supported source to get the picture image from.
240    * Supported sources:
241    * HttpServletRequest e.g: 'request' var from controller to run getFile('file') against.
242    * ServletContextResource e.g: grailsApplication.mainContext.getResource('images/logo.png')
243    * File e.g: new File('picture.jpg')
244    */
245    def savePicture(params, pictureSource) {
246        InventoryItem.withTransaction { status ->
247            def result = [:]
248
249            def kByteMultiplier = 1000
250
251            def fail = { Map m ->
252                status.setRollbackOnly()
253                if(result.inventoryItemInstance && m.field)
254                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
255                result.error = [ code: m.code, args: m.args ?: ["InventoryItem", params.id] ]
256                return result
257            }
258
259            result.inventoryItemInstance = InventoryItem.get(params.id)
260
261            if(!result.inventoryItemInstance)
262                return fail(code:"default.not.found")
263
264            // Optimistic locking check.
265            if(params.version) {
266                if(result.inventoryItemInstance.version > params.version.toLong())
267                    return fail(field:"version", code:"default.optimistic.locking.failure")
268            }
269
270            if(result.inventoryItemInstance.picture)
271                return fail(field:"picture", code:"inventory.item.already.has.picture")
272
273            // Declare some more variables, since we appear to have most of what we need.
274            def picture = new Picture(inventoryItem: result.inventoryItemInstance)
275            def imaging = new Imaging()
276            def images = null
277            def pictureFile
278            def pictureFileName = ''
279            def pictureInputStream
280
281            // Check the supplied pictureSource and get the inputStream.
282            if(pictureSource instanceof javax.servlet.http.HttpServletRequest) {
283                def multiPartFile = pictureSource.getFile('file')
284                pictureFileName = multiPartFile.originalFilename
285
286                if(!multiPartFile || multiPartFile.isEmpty())
287                    return fail(code: "default.file.not.supplied")
288
289                if (multiPartFile.getSize() > Image.MAX_SIZE)
290                    return fail(code: "default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
291
292                pictureInputStream = multiPartFile.inputStream
293            }
294            else if(pictureSource instanceof org.springframework.web.context.support.ServletContextResource) {
295                pictureFile = pictureSource.getFile()
296                pictureFileName = pictureFile.name
297
298                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
299                    return fail(code:"default.file.not.supplied")
300
301                if (pictureFile.length() > Image.MAX_SIZE)
302                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
303
304                pictureInputStream = pictureSource.inputStream
305            }
306            else if(pictureSource instanceof File) {
307                pictureFile = pictureSource
308                pictureFileName = pictureFile.name
309
310                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
311                    return fail(code:"default.file.not.supplied")
312
313                if (pictureFile.length() > Image.MAX_SIZE)
314                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
315
316                pictureInputStream = new FileInputStream(pictureSource)
317            }
318            else {
319                    return fail(code:"inventory.item.picture.source.not.supported")
320            }
321
322            // Create the Images.
323            try {
324                images = imaging.createAll(result.inventoryItemInstance, picture, pictureInputStream)
325                // Ensure the stream is closed.
326                pictureInputStream.close()
327            }
328            catch(Exception ex) {
329                log.error("picture save", ex)
330                // Ensure the stream is closed.
331                pictureInputStream.close()
332                return fail(code:"inventory.item.picture.file.unrecognised", args: [pictureFileName])
333            }
334
335            // Add images to picture.
336            images.each { image ->
337                picture.addToImages(image)
338            }
339
340            // Save picture.
341            if(picture.hasErrors() || !picture.save())
342                return fail(code:"default.create.failure", args: ["Picture"])
343
344            result.inventoryItemInstance.picture = picture
345
346            // Save inventoryItem.
347            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
348                return fail(code:"default.create.failure")
349
350            // success
351            return result
352
353        } // end withTransaction
354    } // savePicture
355
356    /**
357    * Import inventory pictures from an uploaded zip file or picture.
358    * @param request The http request to run getFile against.
359    * Get file should return a zip format file containing the inventory item pictures or a picture file.
360    */
361    def importInventoryItemPictures(request) {
362            def result = [:]
363
364            def kByteMultiplier = 1000
365            def mByteMultiplier = 1000 * kByteMultiplier
366            def fileMaxSize = 100 * mByteMultiplier
367
368            def fail = { Map m ->
369                result.error = [ code: m.code, args: m.args ]
370                return result
371            }
372
373            // Get file from request.
374            def multiPartFile = request.getFile('file')
375            def uploadedFileName = multiPartFile.originalFilename
376
377            if(!multiPartFile || multiPartFile.isEmpty())
378                return fail(code: "default.file.not.supplied")
379
380            if (multiPartFile.getSize() > fileMaxSize)
381                return fail(code: "default.file.over.max.size", args: [fileMaxSize/mByteMultiplier, "MB"])
382
383            // Check and create import dir.
384            def dir = new File(ConfigurationHolder.config.globalDirs.tempInventoryItemPicturesDirectory)
385
386            if(!dir.exists())
387                dir.mkdirs()
388
389            if(!dir.isDirectory()) {
390                return fail(code:'inventoryItemPictures.import.failure.no.directory')
391            }
392
393            // Write file to disk.
394            def diskFile = new File(dir.absolutePath + File.separator + uploadedFileName)
395            multiPartFile.transferTo(diskFile)
396
397            // File patterns
398            def zipFilePattern = ~/[^\s].*(\.(?i)(zip))$/
399            def pictureFilePattern = ~/[^\s].*(\.(?i)(jpg|png|gif|bmp))$/
400
401            // If file claims to be a zip file then try using ant to unzip.
402            if(diskFile.name.matches(zipFilePattern)) {
403                def ant = new AntBuilder()
404                try {
405                    ant.unzip(  src: diskFile.absolutePath,
406                                        dest: dir.absolutePath,
407                                        overwrite:"true" )
408                }
409                catch(e) {
410                    log.error e
411                    return fail(code:'inventoryItemPictures.import.failure.to.unzip')
412                }
413            }
414
415            // Recurse through dir building list of pictureFiles.
416            def pictureFiles = []
417            dir.eachFileMatch(pictureFilePattern) {
418                pictureFiles << it
419            }
420
421            dir.eachDirRecurse { subDir ->
422                subDir.eachFileMatch(pictureFilePattern) {
423                    pictureFiles << it
424                }
425            }
426
427            pictureFiles.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }
428
429            // Find inventoryItems by name of picture and call savePicture.
430            def inventoryItemInstance
431            def itemName
432            def savePictureResult
433            def pictureCount = 0
434            def picturesSavedCount = 0
435
436            // Turn off index mirroring.
437            createDataService.stopSearchableIndex()
438
439            for(pictureFile in pictureFiles) {
440                pictureCount++
441
442                if(pictureCount % 10 == 0) {
443                    cleanUpGorm()
444                }
445
446                itemName = WordUtils.capitalize(pictureFile.name[0..-5])
447                inventoryItemInstance = InventoryItem.findByName(itemName)
448                if(!inventoryItemInstance) {
449                    log.warn 'InventoryItem not found with name: ' + itemName
450                    continue
451                }
452                if(inventoryItemInstance.picture) {
453                    log.warn 'InventoryItem already has picture: ' + itemName
454                    continue
455                }
456                savePictureResult = savePicture(inventoryItemInstance, pictureFile)
457                if(savePictureResult.error)
458                    log.error savePictureResult.error
459                else {
460                    picturesSavedCount++
461                    log.info 'InventoryItem picture saved: ' + itemName
462                }
463            }
464
465            // Start mirroring again and rebuild index.
466            createDataService.startSearchableIndex()
467
468            log.info 'InventoryItem pictures saved: ' + picturesSavedCount
469            log.info 'InventoryItem pictures total: ' + pictureCount
470
471            // Cleanup.
472            dir.eachFile() {
473                if(it.isDirectory())
474                    it.deleteDir()
475                else
476                    it.delete()
477            }
478
479            // Success.
480            return result
481
482    } // importInventoryItemPictures
483
484    /**
485    * This cleans up the hibernate session and a grails map.
486    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
487    * The hibernate session flush is normal for hibernate.
488    * The map is apparently used by grails for domain object validation errors.
489    * A starting point for clean up is every 100 objects.
490    */
491    def cleanUpGorm() {
492        def session = sessionFactory.currentSession
493        session.flush()
494        session.clear()
495        propertyInstanceMap.get().clear()
496    }
497
498} // end class
Note: See TracBrowser for help on using the repository browser.