001    /**
002     * Copyright 2005-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kns.web.struts.action;
017    
018    import org.apache.commons.collections.CollectionUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.apache.struts.action.ActionForm;
021    import org.apache.struts.action.ActionForward;
022    import org.apache.struts.action.ActionMapping;
023    import org.apache.struts.action.RedirectingActionForward;
024    import org.kuali.rice.core.api.util.RiceConstants;
025    import org.kuali.rice.core.api.util.RiceKeyConstants;
026    import org.kuali.rice.kim.api.KimConstants;
027    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
028    import org.kuali.rice.kns.document.MaintenanceDocumentBase;
029    import org.kuali.rice.kns.inquiry.Inquirable;
030    import org.kuali.rice.kns.util.WebUtils;
031    import org.kuali.rice.kns.web.struts.form.InquiryForm;
032    import org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase;
033    import org.kuali.rice.kns.web.ui.Field;
034    import org.kuali.rice.kns.web.ui.Row;
035    import org.kuali.rice.kns.web.ui.Section;
036    import org.kuali.rice.krad.bo.Attachment;
037    import org.kuali.rice.krad.bo.BusinessObject;
038    import org.kuali.rice.krad.bo.DocumentAttachment;
039    import org.kuali.rice.krad.bo.Exporter;
040    import org.kuali.rice.krad.bo.MultiDocumentAttachment;
041    import org.kuali.rice.krad.bo.Note;
042    import org.kuali.rice.krad.bo.PersistableAttachment;
043    import org.kuali.rice.krad.bo.PersistableAttachmentList;
044    import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
045    import org.kuali.rice.krad.exception.AuthorizationException;
046    import org.kuali.rice.krad.service.KRADServiceLocator;
047    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
048    import org.kuali.rice.krad.service.ModuleService;
049    import org.kuali.rice.krad.service.NoteService;
050    import org.kuali.rice.krad.util.GlobalVariables;
051    import org.kuali.rice.krad.util.KRADConstants;
052    import org.kuali.rice.krad.util.KRADUtils;
053    
054    import javax.servlet.http.HttpServletRequest;
055    import javax.servlet.http.HttpServletResponse;
056    import java.io.ByteArrayOutputStream;
057    import java.io.IOException;
058    import java.lang.reflect.Method;
059    import java.util.Collections;
060    import java.util.List;
061    import java.util.Map;
062    
063    /**
064     * This class handles actions for inquiries of business objects.
065     */
066    public class KualiInquiryAction extends KualiAction {
067        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiInquiryAction.class);
068        private NoteService noteService;
069    
070        @Override
071        protected void checkAuthorization(ActionForm form, String methodToCall) throws AuthorizationException {
072            if (!(form instanceof InquiryForm)) {
073                super.checkAuthorization(form, methodToCall);
074            } else {
075                try {
076                    if(!KRADConstants.DOWNLOAD_BO_ATTACHMENT_METHOD.equals(methodToCall)){
077                            Class businessObjectClass = Class.forName(((InquiryForm) form).getBusinessObjectClassName());
078                            if (!KimApiServiceLocator.getPermissionService().isAuthorizedByTemplate(
079                                GlobalVariables.getUserSession().getPrincipalId(), KRADConstants.KNS_NAMESPACE,
080                                KimConstants.PermissionTemplateNames.INQUIRE_INTO_RECORDS,
081                                KRADUtils.getNamespaceAndComponentSimpleName(businessObjectClass),
082                                Collections.<String, String>emptyMap())) {
083    
084                                    throw new AuthorizationException(GlobalVariables.getUserSession().getPerson().getPrincipalName(), 
085                                    "inquire",
086                                    businessObjectClass.getSimpleName());
087                            }
088                    }
089                }
090                catch (ClassNotFoundException e) {
091                    LOG.warn("Unable to load BusinessObject class: " + ((InquiryForm) form).getBusinessObjectClassName(), e);
092                    super.checkAuthorization(form, methodToCall);
093                }
094            }
095        }
096    
097        @Override
098            protected Map<String, String> getRoleQualification(ActionForm form,
099                            String methodToCall) {
100                    Map<String, String> roleQualification = super.getRoleQualification(
101                                    form, methodToCall);
102                    if (form instanceof InquiryForm) {
103                            Map<String, String> primaryKeys = ((InquiryForm) form)
104                                            .getInquiryPrimaryKeys();
105                            if (primaryKeys != null) {
106                                    for (String keyName : primaryKeys.keySet()) {
107                                            roleQualification.put(keyName, primaryKeys.get(keyName));                                       
108                                    }
109                            }
110                    }
111                    return roleQualification;
112            }
113    
114            @Override
115        public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
116            request.setAttribute(KRADConstants.PARAM_MAINTENANCE_VIEW_MODE, KRADConstants.PARAM_MAINTENANCE_VIEW_MODE_INQUIRY);
117            return super.execute(mapping, form, request, response);
118        }
119    
120        /**
121         * Gets an inquirable impl from the impl service name parameter. Then calls lookup service to retrieve the record from the
122         * key/value parameters. Finally gets a list of Rows from the inquirable
123         */
124        public ActionForward start(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
125            InquiryForm inquiryForm = (InquiryForm) form;
126            if (inquiryForm.getBusinessObjectClassName() == null) {
127                LOG.error("Business object name not given.");
128                throw new RuntimeException("Business object name not given.");
129            }
130            
131            Class boClass = Class.forName(inquiryForm.getBusinessObjectClassName());
132            ModuleService responsibleModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(boClass);
133                    if(responsibleModuleService!=null && responsibleModuleService.isExternalizable(boClass)){
134                            String redirectUrl = responsibleModuleService.getExternalizableBusinessObjectInquiryUrl(boClass, (Map<String, String[]>) request.getParameterMap());
135                            ActionForward redirectingActionForward = new RedirectingActionForward(redirectUrl);
136                            redirectingActionForward.setModule("/");
137                            return redirectingActionForward;
138                    }
139    
140                    return continueWithInquiry(mapping, form, request, response);
141        }
142    
143    
144        /**
145         * Downloads the attachment to the user's browser
146         *
147         * @param mapping
148         * @param form
149         * @param request
150         * @param response
151         * @return ActionForward
152         * @throws Exception
153         */
154        public ActionForward downloadAttachment(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
155            InquiryForm inquiryForm = (InquiryForm) form;
156            int line = getSelectedLine(request);
157    
158            BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
159            if (line < 0) {
160                if (bo instanceof PersistableAttachment) {
161                    PersistableAttachment attachment = (PersistableAttachment)bo;
162                    if (StringUtils.isNotBlank(attachment.getFileName())
163                            && attachment.getAttachmentContent() != null) {
164                        streamToResponse(attachment.getAttachmentContent(), attachment.getFileName(), attachment.getContentType(), response);
165                    }
166                }
167            } else {
168                if (bo instanceof PersistableAttachmentList) {
169                    PersistableAttachmentList<PersistableAttachment> attachmentsBo = (PersistableAttachmentList<PersistableAttachment>)bo;
170                    if (CollectionUtils.isEmpty(attachmentsBo.getAttachments())) {
171                        return null;
172                    }
173    
174                    List<? extends PersistableAttachment> attachments = attachmentsBo.getAttachments();
175                    if (CollectionUtils.isNotEmpty(attachments)
176                            && attachments.size() > line) {
177                        PersistableAttachment attachment = (PersistableAttachment)attachmentsBo.getAttachments().get(line);
178                        streamToResponse(attachment.getAttachmentContent(), attachment.getFileName(), attachment.getContentType(), response);
179                    }
180                }
181            }
182            return null;
183        }
184    
185        public ActionForward downloadCustomBOAttachment(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
186            String fileName = request.getParameter(KRADConstants.BO_ATTACHMENT_FILE_NAME);
187                    String contentType = request.getParameter(KRADConstants.BO_ATTACHMENT_FILE_CONTENT_TYPE);
188                    String fileContentBoField = request.getParameter(KRADConstants.BO_ATTACHMENT_FILE_CONTENT_FIELD);
189            //require certain request properties
190            if (fileName != null
191                            && contentType != null
192                            && fileContentBoField != null) {
193                    //make sure user has authorization to download attachment
194                    checkAuthorization(form, findMethodToCall(form, request));
195                    
196                    fileName = StringUtils.replace(fileName, " ", "_");
197                    
198                    InquiryForm inquiryForm = (InquiryForm) form;
199                    BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
200                    checkBO(bo);
201                    
202                    Class clazz = (bo.getClass());
203                    Method method = clazz.getMethod("get"+StringUtils.capitalize(fileContentBoField));
204                    byte[] fileContents = (byte[]) method.invoke(bo);
205                    streamToResponse(fileContents, fileName, contentType,response);
206            } else {
207                    if (fileName == null) {
208                            LOG.error("Request Parameter \""+ KRADConstants.BO_ATTACHMENT_FILE_NAME + "\" not provided.");
209                    }
210                    if (contentType == null) {
211                            LOG.error("Request Parameter \""+ KRADConstants.BO_ATTACHMENT_FILE_CONTENT_TYPE + "\" not provided.");
212                    }
213                    if (fileContentBoField == null) {
214                            LOG.error("Request Parameter \""+ KRADConstants.BO_ATTACHMENT_FILE_CONTENT_FIELD + "\" not provided.");
215                    }
216            }
217            return null;
218        }
219        
220        
221        /**
222         * Downloads the selected attachment to the user's browser
223         *
224         * @param mapping
225         * @param form
226         * @param request
227         * @param response
228         * @return ActionForward
229         * @throws Exception
230         */
231        public ActionForward downloadBOAttachment(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
232            Long noteIdentifier = Long.valueOf(request.getParameter(KRADConstants.NOTE_IDENTIFIER));
233    
234            Note note = this.getNoteService().getNoteByNoteId(noteIdentifier);
235            if(note != null){
236                    Attachment attachment = note.getAttachment();
237                    if(attachment != null){
238                            //make sure attachment is setup with backwards reference to note (rather then doing this we could also just call the attachment service (with a new method that took in the note)
239                            attachment.setNote(note);
240                            WebUtils.saveMimeInputStreamAsFile(response, attachment.getAttachmentMimeTypeCode(), attachment.getAttachmentContents(), attachment.getAttachmentFileName(), attachment.getAttachmentFileSize().intValue());
241                    }
242                    return null;
243            }
244            
245            return mapping.findForward(RiceConstants.MAPPING_BASIC);
246        }
247        
248        public ActionForward continueWithInquiry(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
249            InquiryForm inquiryForm = (InquiryForm) form;
250            
251            if (inquiryForm.getBusinessObjectClassName() == null) {
252                    LOG.error("Business object name not given.");
253                    throw new RuntimeException("Business object name not given.");
254            }
255            
256            BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
257            checkBO(bo);
258            
259            populateSections(mapping, request, inquiryForm, bo);
260            
261            return mapping.findForward(RiceConstants.MAPPING_BASIC);
262        }
263        
264        /**
265         * Turns on (or off) the inactive record display for a maintenance collection.
266         */
267        public ActionForward toggleInactiveRecordDisplay(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
268            InquiryForm inquiryForm = (InquiryForm) form;
269            if (inquiryForm.getBusinessObjectClassName() == null) {
270                LOG.error("Business object name not given.");
271                throw new RuntimeException("Business object name not given.");
272            }
273            
274            BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
275            checkBO(bo);
276            
277            Inquirable kualiInquirable = inquiryForm.getInquirable();
278            //////////////////////////////
279            String collectionName = extractCollectionName(request, KRADConstants.TOGGLE_INACTIVE_METHOD);
280            if (collectionName == null) {
281                LOG.error("Unable to get find collection name in request.");
282                throw new RuntimeException("Unable to get find collection class in request.");
283            }  
284            String parameterName = (String) request.getAttribute(KRADConstants.METHOD_TO_CALL_ATTRIBUTE);
285            boolean showInactive = Boolean.parseBoolean(StringUtils.substringBetween(parameterName, KRADConstants.METHOD_TO_CALL_BOPARM_LEFT_DEL, "."));
286            kualiInquirable.setShowInactiveRecords(collectionName, showInactive);
287            //////////////////////////////
288            
289            populateSections(mapping, request, inquiryForm, bo);
290            
291            if (showInactive) {
292                    reopenInactiveRecords(inquiryForm, collectionName);
293            }
294            
295            return mapping.findForward(RiceConstants.MAPPING_BASIC);
296        }
297        
298        /**
299         * Attempts to reopen sub tabs which would have been closed for inactive records
300         * 
301         * @param inquiryForm the form to reopen records on
302         * @param collectionName the name of the collection reopening
303         */
304        protected void reopenInactiveRecords(InquiryForm inquiryForm, String collectionName) {
305            for (Object sectionAsObject : inquiryForm.getSections()) {
306                    final Section section = (Section)sectionAsObject;
307                    for (Row row: section.getRows()) {
308                            for (Field field : row.getFields()) {
309                                    if (field.getFieldType().equals(Field.CONTAINER) && field.getContainerName().startsWith(collectionName)) {
310                                            final String tabKey = WebUtils.generateTabKey(generateCollectionSubTabName(field));
311                                            inquiryForm.getTabStates().put(tabKey, "OPEN");
312                                    }
313                            }
314                    }
315            }
316        }
317        
318        /**
319         * Finds a container field's sub tab name
320         * 
321         * @param field the collection sub tab name to  
322         * @return the sub tab name
323         */
324        protected String generateCollectionSubTabName(Field field) {
325            final String containerName = field.getContainerElementName();
326            final String cleanedContainerName = 
327                    (containerName == null) ?
328                                    "" :
329                                    containerName.replaceAll("\\d+", "");
330            StringBuilder subTabName = new StringBuilder(cleanedContainerName);
331            for (Field containerField : field.getContainerDisplayFields()) {
332                    subTabName.append(containerField.getPropertyValue());
333            }
334            return subTabName.toString();
335        }
336        
337        @Override
338        public ActionForward toggleTab(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
339            InquiryForm inquiryForm = (InquiryForm) form;
340            if (inquiryForm.getBusinessObjectClassName() == null) {
341                LOG.error("Business object name not given.");
342                throw new RuntimeException("Business object name not given.");
343            }
344            
345            BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
346            checkBO(bo);
347            
348            populateSections(mapping, request, inquiryForm, bo);
349            
350            Inquirable kualiInquirable = inquiryForm.getInquirable();
351            
352            return super.toggleTab(mapping, form, request, response);
353        }
354        
355        
356        
357        /**
358             * @see org.kuali.rice.krad.web.struts.action.KualiAction#hideAllTabs(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
359             */
360            @Override
361            public ActionForward hideAllTabs(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
362            // KULRICE-2852: Overrides hideAllTabs() so that it also calls the populateSections() method.
363                    InquiryForm inquiryForm = (InquiryForm) form;
364            if (inquiryForm.getBusinessObjectClassName() == null) {
365                LOG.error("Business object name not given.");
366                throw new RuntimeException("Business object name not given.");
367            }
368            
369            BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
370            checkBO(bo);
371            
372            populateSections(mapping, request, inquiryForm, bo);
373                    
374                    return super.hideAllTabs(mapping, form, request, response);
375            }
376    
377            /**
378             * @see org.kuali.rice.krad.web.struts.action.KualiAction#showAllTabs(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
379             */
380            @Override
381            public ActionForward showAllTabs(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
382            // KULRICE-2852: Overrides showAllTabs() so that it also calls the populateSections() method.
383                    InquiryForm inquiryForm = (InquiryForm) form;
384            if (inquiryForm.getBusinessObjectClassName() == null) {
385                LOG.error("Business object name not given.");
386                throw new RuntimeException("Business object name not given.");
387            }
388            
389            BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
390            checkBO(bo);
391            
392            populateSections(mapping, request, inquiryForm, bo);
393                    
394                    return super.showAllTabs(mapping, form, request, response);
395            }
396    
397            /**
398         * Handles exporting the BusinessObject for this Inquiry to XML if it has a custom XML exporter available.
399         */
400        public ActionForward export(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
401            InquiryForm inquiryForm = (InquiryForm) form;
402            if (inquiryForm.isCanExport()) {
403                    BusinessObject bo = retrieveBOFromInquirable(inquiryForm);
404                    checkBO(bo);
405                    if (bo != null) {
406                            BusinessObjectEntry businessObjectEntry = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(inquiryForm.getBusinessObjectClassName());
407                            Class<? extends Exporter> exporterClass = businessObjectEntry.getExporterClass();
408                            if (exporterClass != null) {
409                                    Exporter exporter = exporterClass.newInstance();
410                                    response.setContentType(KRADConstants.XML_MIME_TYPE);
411                                    response.setHeader("Content-disposition", "attachment; filename=export.xml");
412                                    exporter.export(businessObjectEntry.getBusinessObjectClass(), Collections.singletonList(bo), KRADConstants.XML_FORMAT, response.getOutputStream());
413                            }
414                    } else {
415                            //show the empty section with error
416                            populateSections(mapping, request, inquiryForm, bo);
417                            return mapping.findForward(RiceConstants.MAPPING_BASIC); 
418                    }
419            }
420            
421            return null;
422        }
423    
424        /**
425         * Convert a Request into a Map<String,String>. Technically, Request parameters do not neatly translate into a Map of Strings,
426         * because a given parameter may legally appear more than once (so a Map of String[] would be more accurate.) This method should
427         * be safe for business objects, but may not be reliable for more general uses.
428         */
429        protected String extractCollectionName(HttpServletRequest request, String methodToCall) {
430            // collection name and underlying object type from request parameter
431            String parameterName = (String) request.getAttribute(KRADConstants.METHOD_TO_CALL_ATTRIBUTE);
432            String collectionName = null;
433            if (StringUtils.isNotBlank(parameterName)) {
434                collectionName = StringUtils.substringBetween(parameterName, methodToCall + ".", ".(");
435            }
436            return collectionName;
437        }
438        
439        protected BusinessObject retrieveBOFromInquirable(InquiryForm inquiryForm) {
440            Inquirable kualiInquirable = inquiryForm.getInquirable();
441            // retrieve the business object
442            BusinessObject bo = kualiInquirable.getBusinessObject(inquiryForm.retrieveInquiryDecryptedPrimaryKeys());
443            if (bo == null) {
444                LOG.error("No records found in inquiry action.");
445                GlobalVariables.getMessageMap().putError(KRADConstants.GLOBAL_ERRORS, RiceKeyConstants.ERROR_INQUIRY);
446            }
447            return bo;
448        }
449        
450        protected void populateSections(ActionMapping mapping, HttpServletRequest request, InquiryForm inquiryForm, BusinessObject bo) {
451            Inquirable kualiInquirable = inquiryForm.getInquirable();
452            
453            if (bo != null) {
454                    // get list of populated sections for display
455                    List<Section> sections = kualiInquirable.getSections(bo);
456                    inquiryForm.setSections(sections);
457                    kualiInquirable.addAdditionalSections(sections, bo);
458            } else {
459                    inquiryForm.setSections(getEmptySections(kualiInquirable.getTitle()));
460            }
461    
462            request.setAttribute(KRADConstants.INQUIRABLE_ATTRIBUTE_NAME, kualiInquirable);
463        }
464        
465        /**
466        *
467        * Handy method to stream the byte array to response object
468        * @param attachmentDataSource
469        * @param response
470        * @throws Exception
471        */
472       protected void streamToResponse(byte[] fileContents, String fileName, String fileContentType,HttpServletResponse response) throws Exception{
473           ByteArrayOutputStream baos = null;
474           try{
475               baos = new ByteArrayOutputStream(fileContents.length);
476               baos.write(fileContents);
477               WebUtils.saveMimeOutputStreamAsFile(response, fileContentType, baos, fileName);
478           }finally{
479               try{
480                   if(baos!=null){
481                       baos.close();
482                       baos = null;
483                   }
484               }catch(IOException ioEx){
485                   LOG.error("Error while downloading attachment");
486                   throw new RuntimeException("IOException occurred while downloading attachment", ioEx);
487               }
488           }
489       }
490        /**
491         * Returns a section list with one empty section and one row.
492         * 
493         * @return list of sections
494         */
495        private List<Section> getEmptySections(String title) {
496            final Row row = new Row(Collections.<Field>emptyList());
497            
498            final Section section = new Section(Collections.singletonList(row));
499                    section.setErrorKey("*");
500                    section.setSectionTitle(title != null ? title : "");
501                    section.setNumberOfColumns(0);
502                    
503                    return Collections.singletonList(section);
504        }
505        
506        /**
507         * throws an exception if BO fails the check.
508         * @param bo the BusinessObject to check.
509         * @throws UnsupportedOperationException if BO is null & no messages have been generated.
510         */
511        private void checkBO(BusinessObject bo) {
512            if (bo == null && GlobalVariables.getMessageMap().hasNoMessages()) {
513                    throw new UnsupportedOperationException("The record you have inquired on does not exist.");
514            }
515        }
516        
517        protected NoteService getNoteService() {
518                    if ( noteService == null ) {
519                            noteService = KRADServiceLocator.getNoteService();
520                    }
521                    return this.noteService;
522            }
523    }