001    /*
002     * Copyright 2011 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.kfs.module.cg.document;
017    
018    import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_ACCOUNTS;
019    import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_PROJECT_DIRECTORS;
020    import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_SUBCONTRACTORS;
021    import static org.kuali.kfs.sys.KFSPropertyConstants.DOCUMENT;
022    import static org.kuali.kfs.sys.KFSPropertyConstants.NEW_MAINTAINABLE_OBJECT;
023    
024    import java.util.ArrayList;
025    import java.util.Collection;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    
030    import org.apache.commons.lang.StringUtils;
031    import org.kuali.kfs.module.cg.businessobject.Award;
032    import org.kuali.kfs.module.cg.businessobject.AwardAccount;
033    import org.kuali.kfs.module.cg.businessobject.AwardOrganization;
034    import org.kuali.kfs.module.cg.businessobject.AwardProjectDirector;
035    import org.kuali.kfs.module.cg.businessobject.AwardSubcontractor;
036    import org.kuali.kfs.module.cg.businessobject.CGProjectDirector;
037    import org.kuali.kfs.module.cg.businessobject.Proposal;
038    import org.kuali.kfs.module.cg.document.validation.impl.AwardRuleUtil;
039    import org.kuali.kfs.sys.KFSConstants;
040    import org.kuali.kfs.sys.KFSKeyConstants;
041    import org.kuali.kfs.sys.KFSPropertyConstants;
042    import org.kuali.kfs.sys.context.SpringContext;
043    import org.kuali.kfs.sys.document.FinancialSystemMaintainable;
044    import org.kuali.rice.kim.bo.Person;
045    import org.kuali.rice.kim.service.PersonService;
046    import org.kuali.rice.kns.bo.DocumentHeader;
047    import org.kuali.rice.kns.bo.PersistableBusinessObject;
048    import org.kuali.rice.kns.document.MaintenanceDocument;
049    import org.kuali.rice.kns.document.MaintenanceLock;
050    import org.kuali.rice.kns.service.BusinessObjectService;
051    import org.kuali.rice.kns.util.GlobalVariables;
052    import org.kuali.rice.kns.util.ObjectUtils;
053    import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument;
054    
055    /**
056     * Methods for the Award maintenance document UI.
057     */
058    public class AwardMaintainableImpl extends FinancialSystemMaintainable {
059        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AwardMaintainableImpl.class);
060        /**
061         * Constructs an AwardMaintainableImpl.
062         */
063        public AwardMaintainableImpl() {
064            super();
065        }
066    
067        /**
068         * Constructs a AwardMaintainableImpl.
069         * 
070         * @param award
071         */
072        public AwardMaintainableImpl(Award award) {
073            super(award);
074            this.setBoClass(award.getClass());
075        }
076    
077        /**
078         * This method is called for refreshing the Agency before display to show the full name in case the agency number was changed by
079         * hand before any submit that causes a redisplay.
080         */
081        @Override
082        public void processAfterRetrieve() {
083            refreshAward(false);
084            super.processAfterRetrieve();
085        }
086    
087        /**
088         * This method is called for refreshing the Agency before a save to display the full name in case the agency number was changed
089         * by hand just before the save.
090         */
091        @Override
092        public void prepareForSave() {
093            refreshAward(false);
094            List<AwardProjectDirector> directors = getAward().getAwardProjectDirectors();
095            if (directors.size() == 1) {
096                directors.get(0).setAwardPrimaryProjectDirectorIndicator(true);
097            }
098            List<AwardOrganization> organizations = getAward().getAwardOrganizations();
099            if (organizations.size() == 1) {
100                organizations.get(0).setAwardPrimaryOrganizationIndicator(true);
101            }
102            // need to populate the synthetic keys for these records
103            // since we can not depend on the keys which exist, we need to determine all those which could match
104            // so we can avoid them
105            List<AwardSubcontractor> awardSubcontractors = getAward().getAwardSubcontractors();
106            if (awardSubcontractors != null && !awardSubcontractors.isEmpty()) {
107                // convert the list into a map of lists containing the used award subcontractor number/amendment number
108                Map<String,List<AwardSubcontractor>> subcontractorAwardMap = new HashMap<String, List<AwardSubcontractor>>();
109                List<AwardSubcontractor> newSubcontractorRecords = new ArrayList<AwardSubcontractor>();
110                for (AwardSubcontractor awardSubcontractor : awardSubcontractors) {
111                    if ( !StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorNumber()) ) {
112                        // already has key - add to map
113                        if ( !subcontractorAwardMap.containsKey(awardSubcontractor.getSubcontractorNumber()) ) {
114                            subcontractorAwardMap.put(awardSubcontractor.getSubcontractorNumber(), new ArrayList<AwardSubcontractor>() );
115                        }
116                        subcontractorAwardMap.get(awardSubcontractor.getSubcontractorNumber()).add(awardSubcontractor);
117                    } else {
118                        // new record, add to new map
119                        newSubcontractorRecords.add(awardSubcontractor);
120                    }
121                }
122                
123                // now, loop over the new records
124                for (AwardSubcontractor awardSubcontractor : newSubcontractorRecords) {
125                    String awardSubcontractorNumber = "1";
126                    String awardSubcontractorAmendmentNumber = "1";
127                    // get the other ones for the same subcontractor
128                    List<AwardSubcontractor> oldSubcontractors = subcontractorAwardMap.get(awardSubcontractor.getSubcontractorNumber());
129                    if ( oldSubcontractors != null ) {
130                        // we have a hit - find the first non-used number                    
131                        // build an array from the unsorted list
132                        boolean[][] nums = new boolean[100][100];
133                        for ( AwardSubcontractor oldSub : oldSubcontractors ) {
134                            try {
135                                nums[Integer.valueOf( oldSub.getAwardSubcontractorNumber() )][Integer.valueOf( oldSub.getAwardSubcontractorAmendmentNumber() )] = true;
136                            } catch ( NumberFormatException ex ) {
137                                // do nothing
138                                LOG.warn( "Unexpected non-integer award subcontractor / amendment number: " + oldSub.getAwardSubcontractorNumber() + " / " + oldSub.getAwardSubcontractorAmendmentNumber() );
139                            }
140                        }
141                        // iterate over the array to get the first empty value
142                        // loop over the awardSubcontractorNumbers first
143                        boolean foundNumbers = false;
144                        for ( int i = 1; i <= 99; i++ ) {
145                            for ( int j = 1; j <= 99; j++ ) {
146                                if ( !nums[j][i] ) { 
147                                    // save the values 
148                                    awardSubcontractorNumber = Integer.toString(j); 
149                                    awardSubcontractorAmendmentNumber = Integer.toString(i); 
150                                    // mark the cell as used before the next pass 
151                                    nums[j][i] = true; 
152                                    // just a flag to allow us to break out of both loops 
153                                    foundNumbers = true; 
154                                    break; 
155                                }
156                            }
157                            if ( foundNumbers ) {
158                                break;
159                            }
160                            // JHK - yes, this will break down if there are more than 9801 subcontracts
161                            // however, the UI will probably break down far before then...
162                        }
163                    }
164                    awardSubcontractor.setAwardSubcontractorNumber(awardSubcontractorNumber);
165                    awardSubcontractor.setAwardSubcontractorAmendmentNumber(awardSubcontractorAmendmentNumber);
166                }
167                
168            }
169            
170            
171    // The implementation below is **** - allows for easy key collisions         
172    //        List<AwardSubcontractor> awardSubcontractors = getAward().getAwardSubcontractors();
173    //        int i = 0;
174    //        if (awardSubcontractors != null && !awardSubcontractors.isEmpty()) {
175    //            for (AwardSubcontractor awardSubcontractor : awardSubcontractors) {
176    //                i++;
177    //                if (StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorAmendmentNumber())) {
178    //                    awardSubcontractor.setAwardSubcontractorAmendmentNumber("" + i);
179    //                }
180    //                if (StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorNumber())) {
181    //                    awardSubcontractor.setAwardSubcontractorNumber("" + i);
182    //                }
183    //            }
184    //        }
185    
186            super.prepareForSave();
187        }
188    
189        /**
190         * This method is called for refreshing the Agency after a lookup to display its full name without AJAX.
191         * 
192         * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#refresh(java.lang.String, java.util.Map,
193         *      org.kuali.rice.kns.document.MaintenanceDocument)
194         */
195        @SuppressWarnings("unchecked")
196        @Override
197        public void refresh(String refreshCaller, Map fieldValues, MaintenanceDocument document) {
198            if (StringUtils.equals("proposalLookupable", (String) fieldValues.get(KFSConstants.REFRESH_CALLER))) {
199    
200                boolean awarded = AwardRuleUtil.isProposalAwarded(getAward());
201                if (awarded) {
202                    String pathToMaintainable = DOCUMENT + "." + NEW_MAINTAINABLE_OBJECT;
203                    GlobalVariables.getMessageMap().addToErrorPath(pathToMaintainable);
204                    GlobalVariables.getMessageMap().putError(KFSPropertyConstants.PROPOSAL_NUMBER, KFSKeyConstants.ERROR_AWARD_PROPOSAL_AWARDED, new String[] { getAward().getProposalNumber().toString() });
205                    GlobalVariables.getMessageMap().removeFromErrorPath(pathToMaintainable);
206                }
207                
208                // SEE KULCG-315 for details on why this code is commented out.
209                // if (AwardRuleUtil.isProposalInactive(getAward())) {
210                // GlobalVariables.getMessageMap().putError(KFSPropertyConstants.PROPOSAL_NUMBER,
211                // KFSKeyConstants.ERROR_AWARD_PROPOSAL_INACTIVE, new String[] { getAward().getProposalNumber().toString() });
212                // }
213    
214                // copy over proposal values after refresh
215                if (!awarded) {
216                    refreshAward(true);
217                    fieldValues.put(KFSConstants.REFERENCES_TO_REFRESH, "proposal");
218                    super.refresh(refreshCaller, fieldValues, document);
219                    getAward().populateFromProposal(getAward().getProposal());
220                    refreshAward(true);
221                }
222            } else {
223                refreshAward(KFSConstants.KUALI_LOOKUPABLE_IMPL.equals(fieldValues.get(KFSConstants.REFRESH_CALLER)));
224                super.refresh(refreshCaller, fieldValues, document);
225            }
226    
227        }
228    
229        /**
230         * Load related objects from the database as needed.
231         * 
232         * @param refreshFromLookup
233         */
234        private void refreshAward(boolean refreshFromLookup) {
235            Award award = getAward();
236            award.refreshNonUpdateableReferences();
237    
238            getNewCollectionLine(AWARD_SUBCONTRACTORS).refreshNonUpdateableReferences();
239            getNewCollectionLine(AWARD_PROJECT_DIRECTORS).refreshNonUpdateableReferences();
240            getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences();
241    
242            // the org list doesn't need any refresh
243            refreshNonUpdateableReferences(award.getAwardOrganizations());
244            refreshNonUpdateableReferences(award.getAwardAccounts());
245            refreshNonUpdateableReferences(award.getAwardSubcontractors());
246            refreshAwardProjectDirectors(refreshFromLookup);
247        }
248    
249        /**
250         * Refresh the collection of associated AwardProjectDirectors.
251         * 
252         * @param refreshFromLookup a lookup returns only the primary key, so ignore the secondary key when true
253         */
254        private void refreshAwardProjectDirectors(boolean refreshFromLookup) {
255            if (refreshFromLookup) {
256                getNewCollectionLine(AWARD_PROJECT_DIRECTORS).refreshNonUpdateableReferences();
257                refreshNonUpdateableReferences(getAward().getAwardProjectDirectors());
258    
259                getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences();
260                refreshNonUpdateableReferences(getAward().getAwardAccounts());
261            }
262            else {
263                refreshWithSecondaryKey((AwardProjectDirector) getNewCollectionLine(AWARD_PROJECT_DIRECTORS));
264                for (AwardProjectDirector projectDirector : getAward().getAwardProjectDirectors()) {
265                    refreshWithSecondaryKey(projectDirector);
266                }
267    
268                refreshWithSecondaryKey((AwardAccount) getNewCollectionLine(AWARD_ACCOUNTS));
269                for (AwardAccount account : getAward().getAwardAccounts()) {
270                    refreshWithSecondaryKey(account);
271                }
272            }
273        }
274    
275        /**
276         * @param collection
277         */
278        private static void refreshNonUpdateableReferences(Collection<? extends PersistableBusinessObject> collection) {
279            for (PersistableBusinessObject item : collection) {
280                item.refreshNonUpdateableReferences();
281            }
282        }
283    
284        /**
285         * Refreshes the reference to ProjectDirector, giving priority to its secondary key. Any secondary key that it has may be user
286         * input, so that overrides the primary key, setting the primary key. If its primary key is blank or nonexistent, then leave the
287         * current reference as it is, because it may be a nonexistent instance which is holding the secondary key (the username, i.e.,
288         * principalName) so we can redisplay it to the user for correction. If it only has a primary key then use that, because it may
289         * be coming from the database, without any user input.
290         * 
291         * @param director the ProjectDirector to refresh
292         */
293        private static void refreshWithSecondaryKey(CGProjectDirector director) {
294            Person cgdir = director.getProjectDirector();
295            if (ObjectUtils.isNotNull(cgdir)) {
296                String secondaryKey = cgdir.getPrincipalName();
297                if (StringUtils.isNotBlank(secondaryKey)) {
298                    Person dir = SpringContext.getBean(PersonService.class).getPersonByPrincipalName(secondaryKey);
299                    director.setPrincipalId(dir == null ? null : dir.getPrincipalId());
300                }
301                if (StringUtils.isNotBlank(director.getPrincipalId())) {
302                    Person person = SpringContext.getBean(PersonService.class).getPerson(director.getPrincipalId());
303                    if (person != null) {
304                        ((PersistableBusinessObject) director).refreshNonUpdateableReferences();
305                    }
306                }
307            }
308        }
309    
310        /**
311         * Gets the underlying Award.
312         * 
313         * @return
314         */
315        public Award getAward() {
316            return (Award) getBusinessObject();
317        }
318    
319        /**
320         * Called for refreshing the {@link Subcontractor} on {@link ProposalSubcontractor} before adding to the proposalSubcontractors
321         * collection on the proposal. this is to ensure that the summary fields are show correctly. i.e. {@link Subcontractor} name
322         * 
323         * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#addNewLineToCollection(java.lang.String)
324         */
325        @Override
326        public void addNewLineToCollection(String collectionName) {
327            refreshAward(false);
328            super.addNewLineToCollection(collectionName);
329        }
330    
331        /**
332         * This method overrides the parent method to check the status of the award document and change the linked
333         * {@link ProposalStatus} to A (Approved) if the {@link Award} is now in approved status.
334         * 
335         * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#doRouteStatusChange(org.kuali.rice.kns.bo.DocumentHeader)
336         */
337        @Override
338        public void doRouteStatusChange(DocumentHeader header) {
339            super.doRouteStatusChange(header);
340    
341            Award award = getAward();
342            KualiWorkflowDocument workflowDoc = header.getWorkflowDocument();
343    
344            // Use the stateIsProcessed() method so this code is only executed when the final approval occurs
345            if (workflowDoc.stateIsProcessed()) {
346                Proposal proposal = award.getProposal();
347                proposal.setProposalStatusCode(Proposal.AWARD_CODE);
348                SpringContext.getBean(BusinessObjectService.class).save(proposal);
349            }
350    
351        }
352    
353        public List<MaintenanceLock> generateMaintenanceLocks() {
354            List<MaintenanceLock> locks = super.generateMaintenanceLocks();
355            return locks;
356        }
357    }