001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kns.web.struts.form;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.apache.struts.upload.FormFile;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.core.api.config.ConfigurationException;
023import org.kuali.rice.kns.authorization.AuthorizationConstants;
024import org.kuali.rice.core.api.encryption.EncryptionService;
025import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
026import org.kuali.rice.kns.document.MaintenanceDocumentBase;
027import org.kuali.rice.kns.inquiry.Inquirable;
028import org.kuali.rice.kns.service.BusinessObjectAuthorizationService;
029import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
030import org.kuali.rice.kns.service.KNSServiceLocator;
031import org.kuali.rice.krad.bo.Exporter;
032import org.kuali.rice.krad.bo.PersistableBusinessObject;
033import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
034import org.kuali.rice.krad.datadictionary.exception.UnknownDocumentTypeException;
035import org.kuali.rice.krad.document.Document;
036import org.kuali.rice.krad.service.DataDictionaryService;
037import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
038import org.kuali.rice.krad.service.KualiModuleService;
039import org.kuali.rice.krad.service.ModuleService;
040import org.kuali.rice.krad.util.KRADConstants;
041import org.kuali.rice.krad.util.ObjectUtils;
042
043import javax.servlet.http.HttpServletRequest;
044import java.lang.reflect.Constructor;
045import java.lang.reflect.InvocationTargetException;
046import java.security.GeneralSecurityException;
047import java.util.ArrayList;
048import java.util.Enumeration;
049import java.util.HashMap;
050import java.util.List;
051import java.util.Map;
052import java.util.regex.Matcher;
053
054/**
055 * This class is the action form for inquiries.
056 */
057public class InquiryForm extends KualiForm {
058    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(InquiryForm.class);
059
060    private static final long serialVersionUID = 1L;
061    private String fieldConversions;
062    private List sections;
063    private String businessObjectClassName;
064    private Map editingMode;
065    private String formKey;
066    private boolean canExport;
067
068    @Override
069    public void addRequiredNonEditableProperties(){
070        super.addRequiredNonEditableProperties();
071        registerRequiredNonEditableProperty(KRADConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE);
072        registerRequiredNonEditableProperty(KRADConstants.DISPATCH_REQUEST_PARAMETER);
073        registerRequiredNonEditableProperty(KRADConstants.DOC_FORM_KEY);
074        registerRequiredNonEditableProperty(KRADConstants.FORM_KEY);
075        registerRequiredNonEditableProperty(KRADConstants.FIELDS_CONVERSION_PARAMETER);
076        registerRequiredNonEditableProperty(KRADConstants.BACK_LOCATION);
077    }
078
079    /**
080     * The following map is used to pass primary key values between invocations of the inquiry screens after the start method has been called.  Values in this map will remain encrypted
081     * if the value was passed in as encrypted
082     */
083    private Map<String, String> inquiryPrimaryKeys;
084
085    private Map<String, String> inquiryDecryptedPrimaryKeys;
086
087    /**
088     * A map of collection name -> Boolean mappings.  Used to denote whether a collection name is configured to show inactive records.
089     */
090    private Map<String, Boolean> inactiveRecordDisplay;
091
092    private Inquirable inquirable;
093
094    public InquiryForm() {
095        super();
096        this.editingMode = new HashMap();
097        this.editingMode.put(AuthorizationConstants.EditMode.VIEW_ONLY, "TRUE");
098        this.inactiveRecordDisplay = null;
099    }
100
101    @Override
102    public void populate(HttpServletRequest request) {
103    // set to null for security reasons (so POJO form base can't access it), then we'll make an new instance of it after
104    // POJO form base is done
105        this.inquirable = null;
106        super.populate(request);
107        if (request.getParameter("returnLocation") != null) {
108            setBackLocation(request.getParameter("returnLocation"));
109        }
110        if (request.getParameter(KRADConstants.DOC_FORM_KEY) != null) {
111            setFormKey(request.getParameter(KRADConstants.DOC_FORM_KEY));
112        }
113        //if the action is download attachment then skip the following populate logic
114        if(!KRADConstants.DOWNLOAD_BO_ATTACHMENT_METHOD.equals(getMethodToCall())){
115                inquirable = getInquirable(getBusinessObjectClassName());
116
117                // the following variable is true if the method to call is not start, meaning that we already called start
118                boolean passedFromPreviousInquiry = !KRADConstants.START_METHOD.equals(getMethodToCall()) && !KRADConstants.CONTINUE_WITH_INQUIRY_METHOD_TO_CALL.equals(getMethodToCall()) && !KRADConstants
119                    .DOWNLOAD_CUSTOM_BO_ATTACHMENT_METHOD.equals(getMethodToCall());
120
121                // There is no requirement that an inquiry screen must display the primary key values.  However, when clicking on hide/show (without javascript) and
122                // hide/show inactive, the PK values are needed to allow the server to properly render results after the user clicks on a hide/show button that results
123                // in server processing.  This line will populate the form with the PK values so that they may be used in subsequent requests.  Note that encrypted
124                // values will remain encrypted in this map.
125                this.inquiryPrimaryKeys = new HashMap<String, String>();
126                this.inquiryDecryptedPrimaryKeys = new HashMap<String, String>();
127
128                populatePKFieldValues(request, getBusinessObjectClassName(), passedFromPreviousInquiry);
129
130                populateInactiveRecordsInIntoInquirable(inquirable, request);
131                populateExportCapabilities(request, getBusinessObjectClassName());
132        }
133    }
134
135    protected Inquirable getInquirable(String boClassName) {
136        try {
137            Class customInquirableClass = null;
138
139            try {
140                BusinessObjectEntry entry = (BusinessObjectEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(boClassName);
141                customInquirableClass = entry.getInquiryDefinition().getInquirableClass();
142            }
143            catch (Exception e) {
144                LOG.error("Unable to correlate business object class with maintenance document entry");
145            }
146
147            Inquirable kualiInquirable = KNSServiceLocator.getKualiInquirable(); // get inquirable impl from Spring
148
149            if (customInquirableClass != null) {
150                Class[] defaultConstructor = new Class[] {};
151                Constructor cons = customInquirableClass.getConstructor(defaultConstructor);
152                kualiInquirable = (Inquirable) cons.newInstance();
153            }
154
155            kualiInquirable.setBusinessObjectClass(Class.forName(boClassName));
156
157            return kualiInquirable;
158        }
159        catch (Exception e) {
160            LOG.error("Error attempting to retrieve inquirable.", e);
161            throw new RuntimeException("Error attempting to retrieve inquirable.");
162        }
163    }
164
165    /**
166     * Gets the alt keys for a class.  Will not return null but and empty list if no keys exist.
167     *
168     * @param clazz the class.
169     * @return the alt keys
170     */
171    private List<List<String>> getAltkeys(Class<?> clazz) {
172        final KualiModuleService kualiModuleService = KRADServiceLocatorWeb.getKualiModuleService();
173        final ModuleService moduleService = kualiModuleService.getResponsibleModuleService(clazz);
174
175        List<List<String>> altKeys = null;
176        if (moduleService != null) {
177                altKeys = moduleService.listAlternatePrimaryKeyFieldNames(clazz);
178        }
179
180        return altKeys != null ? altKeys : new ArrayList<List<String>>();
181    }
182
183    protected void populatePKFieldValues(HttpServletRequest request, String boClassName, boolean passedFromPreviousInquiry) {
184        try {
185            EncryptionService encryptionService = CoreApiServiceLocator.getEncryptionService();
186            DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
187            BusinessObjectAuthorizationService businessObjectAuthorizationService = KNSServiceLocator
188                    .getBusinessObjectAuthorizationService();
189            BusinessObjectMetaDataService businessObjectMetaDataService = KNSServiceLocator
190                    .getBusinessObjectMetaDataService();
191
192            Class businessObjectClass = Class.forName(boClassName);
193
194            // build list of key values from request, if all keys not given throw error
195            List<String> boPKeys = businessObjectMetaDataService.listPrimaryKeyFieldNames(businessObjectClass);
196            final List<List<String>> altKeys = this.getAltkeys(businessObjectClass);
197
198            altKeys.add(boPKeys);
199            boolean bFound = false;
200            for(List<String> boKeys : altKeys ){
201                if(bFound)
202                        break;
203                    int keyCount = boKeys.size();
204                    int foundCount = 0;
205                for (String boKey : boKeys) {
206                    String pkParamName = boKey;
207                        if (passedFromPreviousInquiry) {
208                            pkParamName = KRADConstants.INQUIRY_PK_VALUE_PASSED_FROM_PREVIOUS_REQUEST_PREFIX + pkParamName;
209                        }
210
211                        if (request.getParameter(pkParamName) != null) {
212                                foundCount++;
213                                String parameter = request.getParameter(pkParamName);
214                        
215                        Boolean forceUppercase = Boolean.FALSE;
216                        try {
217                            forceUppercase = dataDictionaryService.getAttributeForceUppercase(businessObjectClass, boKey);
218                        } catch (UnknownBusinessClassAttributeException ex) {
219                            // swallowing exception because this check for ForceUppercase would
220                            // require a DD entry for the attribute.  it is only checking keys
221                            // so most likely there should be an entry.
222                            LOG.warn("BO class " + businessObjectClassName + " property " + boKey + " should probably have a DD definition.", ex);
223                        }
224                        String parameterCopy = parameter;
225                        if (forceUppercase) {
226                                        parameter = parameter.toUpperCase();
227                                }
228
229                        inquiryPrimaryKeys.put(boKey, parameter);
230                        if (businessObjectAuthorizationService.attributeValueNeedsToBeEncryptedOnFormsAndLinks(businessObjectClass, boKey)) {
231                            try {
232                                if(CoreApiServiceLocator.getEncryptionService().isEnabled()) {
233                                    inquiryDecryptedPrimaryKeys.put(boKey, encryptionService.decrypt(parameterCopy));
234                                }
235                            } catch (GeneralSecurityException e) {
236                                LOG.error("BO class " + businessObjectClassName + " property " + boKey + " should have been encrypted, but there was a problem decrypting it.");
237                                throw e;
238                            }
239                        }
240                        else {
241                            inquiryDecryptedPrimaryKeys.put(boKey, parameter);
242                        }
243                    }
244                }
245                if (foundCount == keyCount) {
246                    bFound = true;
247                }
248            }
249            if(!bFound){
250                LOG.error("All keys not given to lookup for bo class name " + businessObjectClass.getName());
251                throw new RuntimeException("All keys not given to lookup for bo class name " + businessObjectClass.getName());
252            }
253        }
254        catch (ClassNotFoundException e) {
255                LOG.error("Can't instantiate class: " + boClassName, e);
256                throw new RuntimeException("Can't instantiate class: " + boClassName);
257        }
258        catch (GeneralSecurityException e) {
259                LOG.error("Can't decrypt value", e);
260                throw new RuntimeException("Can't decrypt value");
261        }
262    }
263
264    /**
265     * Examines the BusinessObject's data dictionary entry to determine if it supports
266     * XML export or not and set's canExport appropriately.
267     */
268    protected void populateExportCapabilities(HttpServletRequest request, String boClassName) {
269        setCanExport(false);
270        BusinessObjectEntry businessObjectEntry = (BusinessObjectEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(boClassName);
271        Class<? extends Exporter> exporterClass = businessObjectEntry.getExporterClass();
272        if (exporterClass != null) {
273                try {
274                        Exporter exporter = exporterClass.newInstance();
275                        if (exporter.getSupportedFormats(businessObjectEntry.getBusinessObjectClass()).contains(KRADConstants.XML_FORMAT)) {
276                                setCanExport(true);
277                        }
278                } catch (Exception e) {
279                        LOG.error("Failed to locate or create exporter class: " + exporterClass, e);
280                        throw new RuntimeException("Failed to locate or create exporter class: " + exporterClass);
281                }
282        }
283    }
284
285
286    /**
287     * @return Returns the fieldConversions.
288     */
289    public String getFieldConversions() {
290        return fieldConversions;
291    }
292
293
294    /**
295     * @param fieldConversions The fieldConversions to set.
296     */
297    public void setFieldConversions(String fieldConversions) {
298        this.fieldConversions = fieldConversions;
299    }
300
301
302    /**
303     * @return Returns the inquiry sections.
304     */
305    public List getSections() {
306        return sections;
307    }
308
309
310    /**
311     * @param sections The sections to set.
312     */
313    public void setSections(List sections) {
314        this.sections = sections;
315    }
316
317    /**
318     * @return Returns the businessObjectClassName.
319     */
320    public String getBusinessObjectClassName() {
321        return businessObjectClassName;
322    }
323
324    /**
325     * @param businessObjectClassName The businessObjectClassName to set.
326     */
327    public void setBusinessObjectClassName(String businessObjectClassName) {
328        this.businessObjectClassName = businessObjectClassName;
329    }
330
331    public Map getEditingMode() {
332        return editingMode;
333    }
334
335    /**
336     * Gets the map used to pass primary key values between invocations of the inquiry screens after the start method has been called.  All field values that were passed in encrypted will
337     * be encrypted in this map
338     *
339     * @return
340     */
341    public Map<String, String> getInquiryPrimaryKeys() {
342        return this.inquiryPrimaryKeys;
343    }
344
345    /**
346     * Gets the map used to pass primary key values between invocations of the inquiry screens after the start method has been called.  All fields will be decrypted
347     *
348     * Purposely not named as a getter, to make it harder for POJOFormBase to access it
349     *
350     * @return
351     */
352    public Map<String, String> retrieveInquiryDecryptedPrimaryKeys() {
353        return this.inquiryDecryptedPrimaryKeys;
354    }
355
356    /**
357     * Sets the map used to pass primary key values between invocations of the inquiry screens after the start method has been called.
358     *
359     * @param inquiryPrimaryKeys
360     */
361    public void setInquiryPrimaryKeys(Map<String, String> inquiryPrimaryKeys) {
362        this.inquiryPrimaryKeys = inquiryPrimaryKeys;
363    }
364
365    /**
366     * Gets map of collection name -> Boolean mappings.  Used to denote whether a collection name is configured to show inactive records.
367     *
368     * @return
369     */
370    public Map<String, Boolean> getInactiveRecordDisplay() {
371        return getInquirable().getInactiveRecordDisplay();
372    }
373
374    public Inquirable getInquirable() {
375        return inquirable;
376    }
377
378    protected void populateInactiveRecordsInIntoInquirable(Inquirable inquirable, HttpServletRequest request) {
379        for (Enumeration e = request.getParameterNames(); e.hasMoreElements();) {
380            String paramName = (String) e.nextElement();
381            if (paramName.startsWith(KRADConstants.INACTIVE_RECORD_DISPLAY_PARAM_PREFIX)) {
382                String collectionName = StringUtils.substringAfter(paramName, KRADConstants.INACTIVE_RECORD_DISPLAY_PARAM_PREFIX);
383                Boolean showInactive = Boolean.parseBoolean(request.getParameter(paramName));
384                inquirable.setShowInactiveRecords(collectionName, showInactive);
385            }
386        }
387    }
388
389    public String getFormKey() {
390        return this.formKey;
391    }
392
393    public void setFormKey(String formKey) {
394        this.formKey = formKey;
395    }
396
397        /**
398         * Returns true if this Inquiry supports XML export of the BusinessObject.
399         */
400        public boolean isCanExport() {
401                return this.canExport;
402        }
403
404        /**
405         * Sets whether or not this Inquiry supports XML export of it's BusinessObject.
406         */
407        public void setCanExport(boolean canExport) {
408                this.canExport = canExport;
409        }
410
411
412}