001    /**
002     * Copyright 2005-2015 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 edu.sampleu.bookstore.document.attribs;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.log4j.Logger;
020    import org.kuali.rice.core.api.CoreApiServiceLocator;
021    import org.kuali.rice.core.api.uif.RemotableAttributeError;
022    import org.kuali.rice.core.api.uif.RemotableAttributeField;
023    import org.kuali.rice.core.api.uif.RemotableDatepicker;
024    import org.kuali.rice.kew.api.KewApiConstants;
025    import org.kuali.rice.kew.api.document.DocumentWithContent;
026    import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
027    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
028    import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
029    import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
030    import org.kuali.rice.kew.api.extension.ExtensionDefinition;
031    import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
032    import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
033    import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
034    import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
035    import org.w3c.dom.Element;
036    import org.xml.sax.InputSource;
037    import org.xml.sax.SAXException;
038    
039    import javax.jws.WebParam;
040    import javax.xml.parsers.DocumentBuilderFactory;
041    import javax.xml.parsers.ParserConfigurationException;
042    import javax.xml.xpath.XPath;
043    import javax.xml.xpath.XPathConstants;
044    import javax.xml.xpath.XPathExpressionException;
045    import java.io.BufferedReader;
046    import java.io.IOException;
047    import java.io.StringReader;
048    import java.math.BigDecimal;
049    import java.math.BigInteger;
050    import java.text.DateFormat;
051    import java.text.ParseException;
052    import java.text.ParsePosition;
053    import java.text.SimpleDateFormat;
054    import java.util.ArrayList;
055    import java.util.Date;
056    import java.util.List;
057    
058    /**
059     * Base class for simple attributes which extract values from document content via an xpath expression.
060     * Compare to {@link org.kuali.rice.kew.docsearch.xml.StandardGenericXMLSearchableAttribute}.
061     * In most cases it's simplest to just define an SGXSA. This class exists expressly to aid testing
062     * non-SGXSA attributes, and illustrates performing proper validation.
063     */
064    public abstract class XPathSearchableAttribute implements SearchableAttribute {
065        protected final Logger log;
066        protected final String key;
067        protected final String title;
068        protected final String xpathExpression;
069        protected final String dataType;
070    
071        protected XPathSearchableAttribute(String key, String dataType, String xpathExpression) {
072            this(key, dataType, xpathExpression, null);
073        }
074    
075        protected XPathSearchableAttribute(String key, String dataType, String xpathExpression, String title) {
076            this.key = key;
077            this.dataType = dataType;
078            this.xpathExpression = xpathExpression;
079            this.log = Logger.getLogger(getClass().getName() + ":" + key);
080            this.title = title == null ? log.getName(): title;
081        }
082        
083        @Override
084        public String generateSearchContent(@WebParam(name = "extensionDefinition") ExtensionDefinition extensionDefinition,
085                                            @WebParam(name = "documentTypeName") String documentTypeName,
086                                            @WebParam(name = "attributeDefinition") WorkflowAttributeDefinition attributeDefinition) {
087            // no custom search content, we just use the document content directly
088            return null;
089        }
090    
091        @Override
092        public List<DocumentAttribute> extractDocumentAttributes(@WebParam(name = "extensionDefinition") ExtensionDefinition extensionDefinition,
093                                                                 @WebParam(name = "documentWithContent") DocumentWithContent documentWithContent) {
094            List<DocumentAttribute> attribs = new ArrayList<DocumentAttribute>(1);
095            String appContent = documentWithContent.getDocumentContent().getApplicationContent();
096            XPath xpath = XPathHelper.newXPath();
097            try {
098                //InputSource source = new StringReader(appContent);
099                Element source = DocumentBuilderFactory
100                        .newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(appContent)))).getDocumentElement();
101                String result = (String) xpath.evaluate(xpathExpression, source, XPathConstants.STRING);
102                // xpath has no concept of null node, missing text values are the empty string
103                if (StringUtils.isNotEmpty(result)) {
104                    try {
105                        attribs.add(createAttribute(this.key, result, this.dataType));
106                    } catch (ParseException pe) {
107                        log.error("Error converting value '" + result + "' to type '" + this.dataType + "'");
108                    }
109                }
110            } catch (XPathExpressionException xep) {
111                log.error("Error evaluating searchable attribute expression: '" + this.xpathExpression + "'", xep);
112            } catch (SAXException se) {
113                log.error("Error parsing application content: '" + appContent + "'", se);
114            } catch (ParserConfigurationException pce) {
115                log.error("Error parsing application content: '" + appContent + "'", pce);
116            } catch (IOException ioe) {
117                log.error("Error parsing application content: '" + appContent + "'", ioe);
118            }
119            return attribs;
120        }
121    
122        /**
123         * Creates an DocumentAttribute of the specified type
124         */
125        protected static DocumentAttribute createAttribute(String name, String value, String dataTypeValue) throws ParseException {
126            if (StringUtils.isBlank(dataTypeValue)) {
127                return DocumentAttributeFactory.createStringAttribute(name, value);
128            } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING.equals(dataTypeValue)) {
129                return DocumentAttributeFactory.createStringAttribute(name, value);
130            } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE.equals(dataTypeValue)) {
131                try {
132                    return DocumentAttributeFactory.createDateTimeAttribute(name, CoreApiServiceLocator.getDateTimeService().convertToDate(value));
133                } catch (ParseException pe) {
134                    // HACK: KRAD is sending us yyyy-MM-dd which is not in the standard format list...
135                    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
136                    dateFormat.setLenient(false);
137                    Date date = dateFormat.parse(value);
138                    return DocumentAttributeFactory.createDateTimeAttribute(name, date);
139                }
140            } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_LONG.equals(dataTypeValue)) {
141                return DocumentAttributeFactory.createIntegerAttribute(name, new BigInteger(value));
142            } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT.equals(dataTypeValue)) {
143                return DocumentAttributeFactory.createDecimalAttribute(name, new BigDecimal(value));
144            }
145            throw new IllegalArgumentException("Invalid dataTypeValue was given: " + dataTypeValue);
146        }
147    
148        @Override
149        public List<RemotableAttributeField> getSearchFields(@WebParam(name = "extensionDefinition") ExtensionDefinition extensionDefinition,
150                                                             @WebParam(name = "documentTypeName") String documentTypeName) {
151            List<RemotableAttributeField> fields = new ArrayList<RemotableAttributeField>();
152            RemotableAttributeField.Builder builder = RemotableAttributeField.Builder.create(key);
153            builder.setLongLabel(this.title);
154            builder.setDataType(DocumentSearchInternalUtils.convertValueToDataType(this.dataType));
155            if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE.equals(this.dataType)) {
156                builder.getWidgets().add(RemotableDatepicker.Builder.create());
157            }
158            builder = decorateRemotableAttributeField(builder);
159            fields.add(builder.build());
160            return fields;
161        }
162    
163        /**
164         * Template method for subclasses to customize the remotableattributefield
165         * @return modified or new RemotableAttributeField.Builder
166         */
167        protected RemotableAttributeField.Builder decorateRemotableAttributeField(RemotableAttributeField.Builder raf) {
168            return raf;
169        }
170    
171        @Override
172        public List<RemotableAttributeError> validateDocumentAttributeCriteria(@WebParam(name = "extensionDefinition") ExtensionDefinition extensionDefinition,
173                                                                               @WebParam(name = "documentSearchCriteria") DocumentSearchCriteria documentSearchCriteria) {
174            SearchableAttributeValue valueType = DocumentSearchInternalUtils.getSearchableAttributeValueByDataTypeString(this.dataType);
175            return DocumentSearchInternalUtils.validateSearchFieldValues(this.key, valueType, documentSearchCriteria.getDocumentAttributeValues().get(key), log.getName(), null, null);
176        }
177    
178    }