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

Last change on this file since 636 was 636, checked in by gav, 14 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.