001 /** 002 * Copyright 2005-2013 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 }