View Javadoc
1   /*
2    * Copyright 2011 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *     
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.openehealth.ipf.commons.xml;
17  
18  import com.saxonica.xqj.SaxonXQConnection;
19  import com.saxonica.xqj.SaxonXQDataSource;
20  import lombok.Getter;
21  import lombok.Setter;
22  import net.sf.saxon.Configuration;
23  import org.openehealth.ipf.commons.core.modules.api.Transmogrifier;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import javax.xml.namespace.QName;
28  import javax.xml.transform.Result;
29  import javax.xml.transform.Source;
30  import javax.xml.xquery.*;
31  import java.io.InputStream;
32  import java.util.Map;
33  import java.util.Map.Entry;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.ConcurrentMap;
36  
37  /**
38   * XQuery transformer similar to the XsltTransmogrifier
39   * 
40   * @author Stefan Ivanov
41   * 
42   * @param <T>
43   *     output type
44   */
45  public class XqjTransmogrifier<T> extends AbstractCachingXmlProcessor<XQPreparedExpression> implements Transmogrifier<Source, T> {
46      private final static Logger LOG = LoggerFactory.getLogger(XqjTransmogrifier.class);
47  
48      private static final ConcurrentMap<String, Loader<XQPreparedExpression>> XQUERY_CACHE = new ConcurrentHashMap<>();
49      private static final Configuration XQUERY_GLOBAL_CONFIG;
50      private static final SaxonXQDataSource DATA_SOURCE;
51      static {
52          XQUERY_GLOBAL_CONFIG = new Configuration();
53          XQUERY_GLOBAL_CONFIG.setURIResolver(new ClasspathUriResolver(XQUERY_GLOBAL_CONFIG.getURIResolver()));
54          DATA_SOURCE = new SaxonXQDataSource(XQUERY_GLOBAL_CONFIG);
55      }
56  
57      private final Class<T> outputFormat;
58      private SaxonXQConnection connection;
59  
60      @Getter @Setter private Map<String, Object> staticParams;
61  
62  
63      @SuppressWarnings("unchecked")
64      public XqjTransmogrifier() {
65          this((Class<T>) String.class);
66      }
67  
68      /**
69       * @param outputFormat
70       *            currently supported: String, Writer, OutputStream
71       */
72      public XqjTransmogrifier(Class<T> outputFormat) {
73          this(outputFormat, null, null);
74      }
75  
76      /**
77       * @param outputFormat
78       *            currently supported: String, Writer, OutputStream
79       * @param classLoader
80       *            class loader for resource retrieval, may be <code>null</code>
81       */
82      public XqjTransmogrifier(Class<T> outputFormat, ClassLoader classLoader) {
83          this(outputFormat, classLoader, null);
84      }
85  
86      /**
87       * @param outputFormat
88       *            currently supported: String, Writer, OutputStream
89       * @param globalParams
90       *            static XQuery parameters
91       */
92      public XqjTransmogrifier(Class<T> outputFormat, Map<String, Object> globalParams) {
93          this(outputFormat, null, globalParams);
94      }
95  
96      /**
97       * @param outputFormat
98       *            currently supported: String, Writer, OutputStream
99       * @param classLoader
100      *            class loader for resource retrieval, may be <code>null</code>
101      * @param globalParams
102      *            static XQuery parameters
103      */
104     public XqjTransmogrifier(
105             Class<T> outputFormat,
106             ClassLoader classLoader,
107             Map<String, Object> globalParams)
108     {
109         super(classLoader);
110         this.outputFormat = outputFormat;
111 
112         if (globalParams != null) {
113             for (Entry<String, Object> entry : globalParams.entrySet()) {
114                 XQUERY_GLOBAL_CONFIG.setConfigurationProperty(entry.getKey(), entry.getValue());
115             }
116         }
117     }
118 
119     @Override
120     protected ConcurrentMap<String, Loader<XQPreparedExpression>> getCache() {
121         return XQUERY_CACHE;
122     }
123 
124     /**
125      * Converts a Source into a typed result using a XQuery processor.
126      * <p>
127      * The XQ resource location is mandatory in the first extra parameter. Other
128      * parameters may be passed as a Map in the second parameter.
129      * 
130      * @see Transmogrifier#zap(java.lang.Object, java.lang.Object[])
131      */
132     @Override
133     public T zap(Source source, Object... params) {
134         ResultHolder<T> accessor = ResultHolderFactory.create(outputFormat);
135         if (accessor == null) throw new IllegalArgumentException("Format " + outputFormat.getClass() + " is not supported");
136         Result result = accessor.createResult();
137         doZap(source, result, params);
138         return accessor.getResult();
139     }
140 
141     private void doZap(Source source, Result result, Object... params) {
142         XQResultSequence seq = null;
143         try {
144             XQPreparedExpression expression = resource(params);
145             expression.bindDocument(XQConstants.CONTEXT_ITEM, source, null);
146             bindExpressionContext(expression, staticParams);
147             bindExpressionContext(expression, resourceParameters(params));
148             seq = expression.executeQuery();
149             seq.writeSequenceToResult(result);
150         } catch (Exception e) {
151             throw new RuntimeException("XQuery processing failed", e);
152         } finally {
153             if (seq != null && !seq.isClosed()) {
154                 try {
155                     seq.close();
156                 } catch (XQException e) {
157                     LOG.trace("XQLTransmogrifier didn't return a value.", e);
158                 }
159             }
160         }
161     }
162 
163     private void bindExpressionContext(XQDynamicContext exp, Map<String, Object> params) throws XQException {
164         if (params == null) {
165             return;
166         }
167         for (Entry<String, Object> entry : params.entrySet()) {
168             if (entry.getKey().equalsIgnoreCase(RESOURCE_LOCATION)) {
169                 continue;
170             }
171             Object value = entry.getValue();
172             if (value instanceof java.lang.String) {
173                 exp.bindString(new QName(entry.getKey()), (String) value, null);
174             } else if (value instanceof javax.xml.transform.Source) {
175                 exp.bindDocument(new QName(entry.getKey()), (Source) entry.getValue(), null);
176             } else if (value instanceof Boolean) {
177                 exp.bindBoolean(new QName(entry.getKey()), (Boolean) entry.getValue(), null);
178             } else {
179                 exp.bindAtomicValue(new QName(entry.getKey()), (String) entry.getValue(), null);
180             }
181         }
182     }
183 
184     /**
185      * This method had to be overridden because {@link XQPreparedExpression} objects
186      * are not thread-safe, thus an additional replication step is necessary.
187      */
188     @Override
189     protected XQPreparedExpression resource(Object... params) throws Exception {
190         XQPreparedExpression expression = super.resource(params);
191         return getConnection().copyPreparedExpression(expression);
192     }
193 
194     @Override
195     public XQPreparedExpression createResource(Object... params) {
196         String resourceLocation = resourceLocation(params);
197         LOG.debug("Create new template for {}", resourceLocation);
198         try {
199             InputStream stream = resourceContent(params).getInputStream();
200             return getConnection().prepareExpression(stream);
201         } catch (Exception e) {
202             throw new IllegalArgumentException("The resource "
203                     + resourceLocation + " is not valid", e);
204         }
205     }
206 
207     synchronized private SaxonXQConnection getConnection() throws XQException {
208         if (connection == null) {
209             connection = (SaxonXQConnection) DATA_SOURCE.getConnection();
210         }
211         return connection;
212     }
213 
214 }