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 edu.sampleu.bookstore.document.attribs;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.uif.RemotableAttributeError;
022import org.kuali.rice.core.api.uif.RemotableAttributeField;
023import org.kuali.rice.core.api.uif.RemotableDatepicker;
024import org.kuali.rice.kew.api.KewApiConstants;
025import org.kuali.rice.kew.api.document.DocumentWithContent;
026import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
027import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
028import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
029import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
030import org.kuali.rice.kew.api.extension.ExtensionDefinition;
031import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
032import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
033import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
034import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
035import org.w3c.dom.Element;
036import org.xml.sax.InputSource;
037import org.xml.sax.SAXException;
038
039import javax.jws.WebParam;
040import javax.xml.parsers.DocumentBuilderFactory;
041import javax.xml.parsers.ParserConfigurationException;
042import javax.xml.xpath.XPath;
043import javax.xml.xpath.XPathConstants;
044import javax.xml.xpath.XPathExpressionException;
045import java.io.BufferedReader;
046import java.io.IOException;
047import java.io.StringReader;
048import java.math.BigDecimal;
049import java.math.BigInteger;
050import java.text.DateFormat;
051import java.text.ParseException;
052import java.text.ParsePosition;
053import java.text.SimpleDateFormat;
054import java.util.ArrayList;
055import java.util.Date;
056import 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 */
064public 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}