001/*
002 * Copyright 2007 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.ole.vnd.businessobject.lookup;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025
026import org.apache.commons.lang.StringUtils;
027import org.kuali.ole.integration.purap.PurchasingAccountsPayableModuleService;
028import org.kuali.ole.sys.OLEConstants;
029import org.kuali.ole.sys.context.SpringContext;
030import org.kuali.ole.vnd.VendorConstants;
031import org.kuali.ole.vnd.VendorKeyConstants;
032import org.kuali.ole.vnd.VendorPropertyConstants;
033import org.kuali.ole.vnd.businessobject.VendorAddress;
034import org.kuali.ole.vnd.businessobject.VendorDetail;
035import org.kuali.ole.vnd.document.service.VendorService;
036import org.kuali.rice.core.web.format.Formatter;
037import org.kuali.rice.coreservice.framework.parameter.ParameterService;
038import org.kuali.rice.kim.api.services.KimApiServiceLocator;
039import org.kuali.rice.kns.lookup.AbstractLookupableHelperServiceImpl;
040import org.kuali.rice.kns.lookup.HtmlData;
041import org.kuali.rice.kns.lookup.HtmlData.AnchorHtmlData;
042import org.kuali.rice.krad.bo.BusinessObject;
043import org.kuali.rice.krad.exception.ValidationException;
044import org.kuali.rice.krad.lookup.CollectionIncomplete;
045import org.kuali.rice.krad.util.BeanPropertyComparator;
046import org.kuali.rice.krad.util.GlobalVariables;
047import org.kuali.rice.krad.util.KRADConstants;
048import org.kuali.rice.krad.util.ObjectUtils;
049import org.kuali.rice.krad.util.UrlFactory;
050
051public class VendorLookupableHelperServiceImpl extends AbstractLookupableHelperServiceImpl {
052    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(VendorLookupableHelperServiceImpl.class);
053
054    protected VendorService vendorService;
055    protected ParameterService parameterService;
056
057    /**
058     * Add custom links to the vendor search results. One to Allow only active parent vendors to create new divisions. Another to
059     * create a link for B2B shopping if PURAP service has been setup to allow for that.
060     *
061     * @see org.kuali.rice.kns.lookup.LookupableHelperService#getCustomActionUrls(org.kuali.rice.krad.bo.BusinessObject,
062     *      java.util.List, java.util.List pkNames)
063     */
064    @Override
065    public List<HtmlData> getCustomActionUrls(BusinessObject businessObject, List pkNames) {
066        VendorDetail vendor = (VendorDetail) businessObject;
067        List<HtmlData> anchorHtmlDataList = new ArrayList<HtmlData>();
068
069        AnchorHtmlData anchorHtmlData = super.getUrlData(businessObject, KRADConstants.MAINTENANCE_EDIT_METHOD_TO_CALL, pkNames);
070        anchorHtmlDataList.add(anchorHtmlData);
071        String documentTypeName = OLEConstants.Vendor.DOCUMENT_TYPE;
072        String nameSpaceCode = OLEConstants.Vendor.VENDOR_NAMESPACE;
073
074        boolean hasPermission = KimApiServiceLocator.getPermissionService().isAuthorized(
075                GlobalVariables.getUserSession().getPerson().getPrincipalId(), nameSpaceCode,
076                OLEConstants.Vendor.CREATE_VENDOR_DIVISION, Collections.<String, String> emptyMap());
077        if (vendor.isVendorParentIndicator() && vendor.isActiveIndicator() && hasPermission ) {
078            // only allow active parent vendors to create new divisions
079            anchorHtmlDataList.add(super.getUrlData(businessObject, OLEConstants.MAINTENANCE_NEWWITHEXISTING_ACTION, VendorConstants.CREATE_DIVISION, pkNames));
080        }
081
082        //Adding a "Shopping" link for B2B vendors.
083        String b2bUrlString = SpringContext.getBean(PurchasingAccountsPayableModuleService.class).getB2BUrlString();
084        if (vendor.isB2BVendor() && StringUtils.isNotBlank(b2bUrlString)) {
085            Properties theProperties = new Properties();
086            theProperties.put("channelTitle", "Shop Catalogs");
087            String backLocation = this.getBackLocation();
088            int lastSlash = backLocation.lastIndexOf("/");
089            String returnUrlForShop = backLocation.substring(0, lastSlash+1) + "portal.do";
090            String href = UrlFactory.parameterizeUrl(returnUrlForShop, theProperties);
091            anchorHtmlDataList.add(new AnchorHtmlData(href + b2bUrlString, null, "shop"));
092        }
093        return anchorHtmlDataList;
094    }
095
096    /**
097     * Used by getActionUrls to print the url on the Vendor Lookup page for the links to edit a Vendor or to create a new division.
098     * We won't provide a link to copy a vendor because we decided it wouldn't make sense to copy a vendor. We should display the
099     * link to create a new division only if the vendor is a parent vendor, and also remove the vendor detail assigned id from the
100     * query string in the link to create a new division. We'll add the vendor detail assigned id in the query string if the vendor
101     * is not a parent, or if the vendor is a parent and the link is not the create new division link (i.e. if the link is "edit").
102     * We'll always add the vendor header id in the query string in all links.
103     *
104     * @see org.kuali.rice.kns.lookup.AbstractLookupableHelperServiceImpl#getActionUrlHref(org.kuali.rice.krad.bo.BusinessObject, java.lang.String, java.util.List)
105     */
106    @Override
107    protected String getActionUrlHref(BusinessObject businessObject, String methodToCall, List pkNames){
108        if (!methodToCall.equals(OLEConstants.COPY_METHOD)) {
109            Properties parameters = new Properties();
110            parameters.put(OLEConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
111            parameters.put(OLEConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE, businessObject.getClass().getName());
112
113            for (Iterator<String> iter = pkNames.iterator(); iter.hasNext();) {
114                String fieldNm = iter.next();
115                if (!fieldNm.equals(VendorPropertyConstants.VENDOR_DETAIL_ASSIGNED_ID) ||
116                        !((VendorDetail) businessObject).isVendorParentIndicator()
117                        || (((VendorDetail) businessObject).isVendorParentIndicator())
118                        && !methodToCall.equals(OLEConstants.MAINTENANCE_NEWWITHEXISTING_ACTION)) {
119                    Object fieldVal = ObjectUtils.getPropertyValue(businessObject, fieldNm);
120                    if (fieldVal == null) {
121                        fieldVal = OLEConstants.EMPTY_STRING;
122                    }
123                    if (fieldVal instanceof java.sql.Date) {
124                        String formattedString = OLEConstants.EMPTY_STRING;
125                        if (Formatter.findFormatter(fieldVal.getClass()) != null) {
126                            Formatter formatter = Formatter.getFormatter(fieldVal.getClass());
127                            formattedString = (String) formatter.format(fieldVal);
128                            fieldVal = formattedString;
129                        }
130                    }
131                    parameters.put(fieldNm, fieldVal.toString());
132                }
133            }
134            return UrlFactory.parameterizeUrl(OLEConstants.MAINTENANCE_ACTION, parameters);
135        } else {
136            return OLEConstants.EMPTY_STRING;
137        }
138    }
139
140    /**
141     * Overrides the getSearchResults in the super class so that we can do some customization in our vendor lookup. For example, for
142     * vendor name as the search criteria, we want to search both the vendor detail table and the vendor alias table for the vendor
143     * name. Display the vendor's default address state in the search results.
144     *
145     * @see org.kuali.rice.kns.lookup.Lookupable#getSearchResults(java.util.Map)
146     */
147    @Override
148    public List<BusinessObject> getSearchResults(Map<String, String> fieldValues) {
149        boolean unbounded = false;
150        super.setBackLocation(fieldValues.get(OLEConstants.BACK_LOCATION));
151        super.setDocFormKey(fieldValues.get(OLEConstants.DOC_FORM_KEY));
152
153        String vendorName = fieldValues.get(VendorPropertyConstants.VENDOR_NAME);
154
155        List<BusinessObject> searchResults = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), fieldValues, unbounded);
156
157        // re-run the query against the vendor name alias field if necessary and merge the results
158        // this could double the returned results for the search, but there is no alternative at present
159        // without refactoring of the lookup service
160        if (StringUtils.isNotEmpty(vendorName)) {
161            // if searching by vendorName, also search in list of alias names
162            fieldValues.put(VendorPropertyConstants.VENDOR_ALIAS_NAME_FULL_PATH, vendorName);
163            // also make sure that we only use active aliases to match the query string
164            fieldValues.put(VendorPropertyConstants.VENDOR_ALIAS_ACTIVE, "Y");
165            fieldValues.remove(VendorPropertyConstants.VENDOR_NAME);
166            List<BusinessObject> searchResults2 = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), fieldValues, unbounded);
167
168            searchResults.addAll(searchResults2);
169            if (searchResults instanceof CollectionIncomplete && searchResults2 instanceof CollectionIncomplete) {
170                ((CollectionIncomplete) searchResults).setActualSizeIfTruncated(((CollectionIncomplete) searchResults).getActualSizeIfTruncated().longValue() + ((CollectionIncomplete) searchResults2).getActualSizeIfTruncated().longValue());
171            }
172        }
173
174        List<BusinessObject> processedSearchResults = new ArrayList();
175
176        // loop through results
177        for (BusinessObject businessObject : searchResults) {
178            VendorDetail vendor = (VendorDetail) businessObject;
179
180            // if its a top level vendor, search for its divisions and add them to the appropriate list then add the vendor to the
181            // return results
182            // if its a division, see if we already have the parent and if not, retrieve it and its divisions then add the parent to
183            // the return results
184
185
186            // If this vendor is not already in the processedSearchResults, let's do further processing (e.g. setting the state for
187            // lookup from default address, etc)
188            // and then add it in the processedSearchResults.
189            if (!processedSearchResults.contains(vendor)) {
190                Map<String, String> tmpValues = new HashMap<String, String>();
191                List<VendorDetail> relatedVendors = new ArrayList();
192                tmpValues.put(VendorPropertyConstants.VENDOR_HEADER_GENERATED_ID, vendor.getVendorHeaderGeneratedIdentifier().toString());
193                relatedVendors = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), tmpValues, unbounded);
194
195                for (VendorDetail tmpVendor : relatedVendors) {
196                    if (ObjectUtils.isNotNull(tmpVendor) && !processedSearchResults.contains(tmpVendor)) {
197                        // populate state from default address
198                        updateDefaultVendorAddress(tmpVendor);
199                        processedSearchResults.add(tmpVendor);
200                    }
201                }
202
203                if (!processedSearchResults.contains(vendor)) {
204                    updateDefaultVendorAddress(vendor);
205                    processedSearchResults.add(vendor);
206                }
207            }
208        }
209
210        for (BusinessObject businessObject : processedSearchResults) {
211            VendorDetail vendor = (VendorDetail) businessObject;
212            if (!vendor.isVendorParentIndicator()) {
213                // find the parent object in the details collection and add that
214                for (BusinessObject tmpObject : processedSearchResults) {
215                    VendorDetail tmpVendor = (VendorDetail) tmpObject;
216                    if (tmpVendor.getVendorHeaderGeneratedIdentifier().equals(vendor.getVendorHeaderGeneratedIdentifier()) && tmpVendor.isVendorParentIndicator()) {
217                        vendor.setVendorName(tmpVendor.getVendorName() + " > " + vendor.getVendorName());
218                        break;
219                    }
220                }
221            }
222        }
223
224        searchResults.clear();
225        searchResults.addAll(processedSearchResults);
226
227        // sort list if default sort column given
228        List<String> defaultSortColumns = getDefaultSortColumns();
229        if (defaultSortColumns.size() > 0) {
230            Collections.sort(searchResults, new BeanPropertyComparator(getDefaultSortColumns(), true));
231        }
232
233        return searchResults;
234    }
235
236    /**
237     * Populates address fields from default address
238     *
239     * @param vendor venodrDetail
240     */
241    private void updateDefaultVendorAddress(VendorDetail vendor) {
242        VendorAddress defaultAddress = vendorService.getVendorDefaultAddress(vendor.getVendorAddresses(), vendor.getVendorHeader().getVendorType().getAddressType().getVendorAddressTypeCode(), "");
243        if (ObjectUtils.isNotNull(defaultAddress)) {
244
245            if (ObjectUtils.isNotNull(defaultAddress.getVendorState())) {
246                vendor.setVendorStateForLookup(defaultAddress.getVendorState().getName());
247            } else {
248                if ( LOG.isDebugEnabled() ) {
249                    LOG.debug( "Warning - unable to retrieve state for " + defaultAddress.getVendorCountryCode() + " / " + defaultAddress.getVendorStateCode() );
250                }
251                vendor.setVendorStateForLookup("");
252            }
253            vendor.setDefaultAddressLine1(defaultAddress.getVendorLine1Address());
254            vendor.setDefaultAddressLine2(defaultAddress.getVendorLine2Address());
255            vendor.setDefaultAddressCity(defaultAddress.getVendorCityName());
256            vendor.setDefaultAddressPostalCode(defaultAddress.getVendorZipCode());
257            vendor.setDefaultAddressStateCode(defaultAddress.getVendorStateCode());
258            vendor.setDefaultAddressInternationalProvince(defaultAddress.getVendorAddressInternationalProvinceName());
259            vendor.setDefaultAddressCountryCode(defaultAddress.getVendorCountryCode());
260            vendor.setDefaultFaxNumber(defaultAddress.getVendorFaxNumber());
261        } else {
262            if ( LOG.isDebugEnabled() ) {
263                LOG.debug( "Warning - default vendor address was null for " + vendor.getVendorNumber() + " / " + vendor.getVendorHeader().getVendorType().getAddressType().getVendorAddressTypeCode() );
264            }
265            vendor.setVendorStateForLookup("");
266        }
267    }
268
269    /**
270     * Overrides a method of the superclass and is now called instead of that one by the Search method of KualiLookupAction when the
271     * Lookupable is of this class. This method first calls the method from the superclass, which should do all the required field
272     * checking, and then goes through all the specific validations which aren't done in at the JSP level. Both the superclass
273     * method and the various validation methods side-effect the adding of errors to the global error map when the input is found to
274     * have an issue.
275     *
276     * @see org.kuali.rice.kns.lookup.AbstractLookupableHelperServiceImpl#validateSearchParameters(java.util.Map)
277     */
278    @Override
279    public void validateSearchParameters(Map fieldValues) {
280        super.validateSearchParameters(fieldValues);
281
282        validateVendorNumber(fieldValues);
283        validateTaxNumber(fieldValues);
284
285        if (GlobalVariables.getMessageMap().hasErrors()) {
286            throw new ValidationException("Error(s) in search criteria");
287        }
288    }
289
290    /**
291     * Validates that the Vendor Number has no more than one dash in it, and does not consist solely of one dash. Then it calls
292     * extractVendorNumberToVendorIds to obtain vendorHeaderGeneratedId and vendorDetailAssignedId and if either one of the ids
293     * cannot be converted to integers, it will add error that the vendor number must be numerics or numerics separated by a dash.
294     *
295     * @param fieldValues a Map containing only those key-value pairs that have been filled in on the lookup
296     */
297    private void validateVendorNumber(Map fieldValues) {
298        String vendorNumber = (String) fieldValues.get(VendorPropertyConstants.VENDOR_NUMBER);
299        if (StringUtils.isNotBlank(vendorNumber)) {
300            int dashPos1 = vendorNumber.indexOf(VendorConstants.DASH);
301            if (dashPos1 > -1) { // There's a dash in the number.
302                if (vendorNumber.indexOf(VendorConstants.DASH, dashPos1 + 1) > -1) { // There can't be more than one.
303                    GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_VNDR_NUM_TOO_MANY_DASHES);
304                }
305                if (vendorNumber.matches("\\-*")) {
306                    GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_VNDR_NUM_DASHES_ONLY);
307                }
308            }
309            extractVendorNumberToVendorIds(fieldValues, vendorNumber);
310        }
311    }
312
313    /**
314     * Parses the vendorNumber string into vendorHeaderGeneratedIdentifier and vendorDetailAssignedIdentifier, validates that both
315     * fields would be able to be converted into integers, if so it will add both fields into the search criterias map in the
316     * fieldValues and remove the vendorNumber from the fieldValues. If the two fields cannot be converted into integers, this
317     * method will add error message to the errorMap in GlobalVariables that the vendor number must be numeric or numerics separated
318     * by a dash.
319     *
320     * @param fieldValues a Map containing only those key-value pairs that have been filled in on the lookup
321     * @param vendorNumber vendor number String
322     */
323    private void extractVendorNumberToVendorIds(Map fieldValues, String vendorNumber) {
324        String vendorHeaderGeneratedIdentifier = null;
325        String vendorDetailAssignedIdentifier = null;
326        int indexOfDash = vendorNumber.indexOf(VendorConstants.DASH);
327        if (indexOfDash < 0) {
328            vendorHeaderGeneratedIdentifier = vendorNumber;
329        }
330        else {
331            vendorHeaderGeneratedIdentifier = vendorNumber.substring(0, indexOfDash);
332            vendorDetailAssignedIdentifier = vendorNumber.substring(indexOfDash + 1, vendorNumber.length());
333        }
334        try {
335            if (StringUtils.isNotEmpty(vendorHeaderGeneratedIdentifier)) {
336                Integer.parseInt(vendorHeaderGeneratedIdentifier);
337            }
338            if (StringUtils.isNotEmpty(vendorDetailAssignedIdentifier)) {
339                Integer.parseInt(vendorDetailAssignedIdentifier);
340            }
341            fieldValues.remove(VendorPropertyConstants.VENDOR_NUMBER);
342            fieldValues.put(VendorPropertyConstants.VENDOR_HEADER_GENERATED_ID, vendorHeaderGeneratedIdentifier);
343            if (StringUtils.isNotEmpty(vendorDetailAssignedIdentifier)) {
344            fieldValues.put(VendorPropertyConstants.VENDOR_DETAIL_ASSIGNED_ID, vendorDetailAssignedIdentifier);
345        }
346        }
347        catch (NumberFormatException headerExc) {
348            GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_VNDR_NUM_NUMERIC_DASH_SEPARATED);
349        }
350    }
351
352    /**
353     * Validates that the tax number is 9 digits long.
354     *
355     * @param fieldValues a Map containing only those key-value pairs that have been filled in on the lookup
356     */
357    private void validateTaxNumber(Map fieldValues) {
358        String taxNumber = (String) fieldValues.get(VendorPropertyConstants.VENDOR_TAX_NUMBER);
359        if (StringUtils.isNotBlank(taxNumber) && (!StringUtils.isNumeric(taxNumber) || taxNumber.length() != 9)) {
360            GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_TAX_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_TAX_NUM_INVALID);
361        }
362    }
363
364    public void setVendorService(VendorService vendorService) {
365        this.vendorService = vendorService;
366    }
367
368    @Override
369    public void setParameterService(ParameterService parameterService) {
370        this.parameterService = parameterService;
371    }
372
373}