View Javadoc

1   /*
2    Copyright (C) 2008 Grid Systems, S.A.
3   
4    This library is free software; you can redistribute it and/or
5    modify it under the terms of the GNU Lesser General Public
6    License as published by the Free Software Foundation; either
7    version 2.1 of the License, or (at your option) any later version.
8   
9    This library is distributed in the hope that it will be useful,
10   but WITHOUT ANY WARRANTY; without even the implied warranty of
11   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   Lesser General Public License for more details.
13  
14   You should have received a copy of the GNU Lesser General Public
15   License along with this library; if not, write to the Free Software
16   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
17  */
18  package com.gridsystems.nextgrid.api.pom.components;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.net.URI;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import javax.xml.namespace.QName;
30  
31  import nextgrid.api.env.ProcessEnvironment;
32  import nextgrid.api.pom.Grounding;
33  import nextgrid.api.pom.GroundingFactory;
34  import nextgrid.api.pom.ProcessException;
35  import nextgrid.api.pom.Reference;
36  import nextgrid.api.pom.Service;
37  
38  import org.apache.commons.io.FileUtils;
39  import org.apache.commons.jxpath.JXPathContext;
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  
43  import com.gridsystems.nextgrid.api.pom.AbstractGroundingImpl;
44  
45  /**
46   * <p>Grounding providing standard operations on the local file-system.</p>
47   *
48   * <h3>Usage</h3>
49   *
50   * <p>This grounding requires the service to be defined with the following
51   * attributes:</p>
52   *
53   * <ul>
54   *   <li><b>urn:grounding#type:</b>
55   *       urn:file-system</li>
56   *   <li><b>urn:grounding#operation:</b>
57   *       &lt;&lt; one of the available operations &gt;&gt;</li>
58   * </ul>
59   *
60   * <p>Convenience constants are provided for the attribute names and
61   * valid values. For example, the following code creates and configures a Service
62   * for the "OP_DELETE_FILE" operation:</p>
63   *
64   * <pre style="border:1px solid gray">
65   *
66   * ProcessFactory factory = ProcessFactoryFinder.getFactory();
67   * Service s = factory.newServiceProcess();
68   * s.setAttribute(new QName("urn:grounding", "type"), "urn:file-system");
69   * s.setAttribute(new QName("urn:grounding", "operation"), "DeleteFile");
70   * </pre>
71   *
72   * <h3>Available Operations</h3>
73   *
74   * <table border="1" cellspacing="0" cellpadding="1">
75   *   <tr><th>Operation</th><th>Operation Name</th><th>Constant</th></tr>
76   *   <tr><td>Delete File<td>DeleteFile<td>OP_DELETE_FILE
77   *   <tr><td>Create File<td>CreateFileFromTemplate<td>OP_CREATE_FROM_TEMPLATE
78   * </table>
79   *
80   * <h4>DeleteFile</h4>
81   *
82   * This operation deletes one or more files in the local file-system. The files
83   * can be regular files or directories, either empty or not.
84   * <p>
85   * All input parameters are processed by this operations. Any parameter that
86   * can be casted to a {@link java.io.File} instance will be deleted. For the
87   * rest, if their contents in string format matches a local file path to an
88   * existent file, it will be deleted.
89   *
90   * <h4>CreateFileFromTemplate</h4>
91   *
92   * <p>This operation creates a file from an optional template. It uses the
93   * following inputs:</p>
94   *
95   * <ul>
96   *   <li>Template: the first of</li>
97   *   <ul>
98   *     <li>Input-Parameter named 'template'
99   *     <li>Attribute named 'urn:grounding#template-path' (considered as a file path)
100  *     <li>Attribute named 'urn:grounding#template' (considered as the template contents)
101  *   </ul>
102  *   <li>Destination File: the first of</li>
103  *   <ul>
104  *     <li>Input-Parameter named 'dest'
105  *     <li>Attribute named 'urn:grounding#dest' (considered as a file path)
106  *   </ul>
107  * </ul>
108  *
109  * <p>If no destination file is defined, a temporary file will be created.</p>
110  *
111  * <p>If found, the resulting file will be returned in an output parameter
112  * called "file".</p>
113  *
114  * @author Rodrigo Ruiz
115  */
116 public class FileSystemGrounding extends AbstractGroundingImpl
117                                  implements GroundingFactory {
118 
119   /**
120    * Serial version UID.
121    */
122   private static final long serialVersionUID = 4080593507919268762L;
123 
124   /**
125    * Map of supported attribute names and descriptions.
126    */
127   private static final Map<QName, String> ATTRIBS;
128 
129   /**
130    * Class logger.
131    */
132   private static final Log LOG = LogFactory.getLog(FileSystemGrounding.class);
133 
134   /**
135    * Operation that deletes a file from the file system.
136    */
137   public static final String OP_DELETE_FILE = "DeleteFile";
138 
139   /**
140    * Operation that creates a file from a template.
141    */
142   public static final String OP_CREATE_FROM_TEMPLATE = "CreateFileFromTemplate";
143 
144   /**
145    * Grounding type.
146    */
147   public static final String TYPE = "urn:file-system";
148 
149   /**
150    * Grounding type.
151    */
152   public static final URI TYPE_URI = URI.create("urn:file-system");
153 
154   static {
155     Map<QName, String> attrs = new HashMap<QName, String>();
156     attrs.put(ATTR_OPERATION, "Operation name");
157     ATTRIBS = Collections.unmodifiableMap(attrs);
158   }
159 
160   /**
161    * Creates a new instance.
162    */
163   public FileSystemGrounding() {
164     super(TYPE_URI, ATTRIBS);
165   }
166 
167   /**
168    * {@inheritDoc}
169    */
170   public void dispose() {
171   }
172 
173   /**
174    * {@inheritDoc}
175    */
176   public void invoke(ProcessEnvironment env, Service service)
177     throws ProcessException {
178 
179     String op = service.getAttribute(ATTR_OPERATION);
180     if (OP_DELETE_FILE.equalsIgnoreCase(op)) {
181       deleteFile(service);
182     } else if (OP_CREATE_FROM_TEMPLATE.equalsIgnoreCase(op)) {
183       createFromTemplate(service);
184     } else {
185       throw new ProcessException("Invalid operation '" + op + "'");
186     }
187   }
188 
189   /**
190    * Deletes the files referenced in the specified inputs.
191    *
192    * @param service Service execution context
193    * @throws ProcessException If an error occurs
194    */
195   private void deleteFile(Service service) throws ProcessException {
196     Map<String, Reference<?>> inputs = service.getInputs();
197 
198     for (String name : inputs.keySet()) {
199       Reference<?> ref = inputs.get(name);
200 
201       try {
202         File f = toFile(ref);
203 
204         if (f == null) {
205           LOG.warn("Skiping null file reference input '" + name + "'");
206         } else {
207           LOG.info("Deleting file " + f.getPath());
208           FileUtils.forceDelete(f);
209         }
210       } catch (IOException e) {
211         throw new ProcessException("Error executing grounding", e);
212       }
213     }
214   }
215 
216   /**
217    * Performs the actions for {@link #OP_CREATE_FROM_TEMPLATE}.
218    *
219    * @param service Service execution context
220    * @throws ProcessException If an error occurs
221    */
222   private void createFromTemplate(Service service) throws ProcessException {
223     try {
224 
225       // determine destination file
226       Reference<?> refDest = service.getInput("dest");
227       File dest = toFile(refDest);
228       if (dest == null) {
229         String path = service.getAttribute(NS_GROUNDING, "dest");
230         if (path != null) {
231           dest = new File(path);
232         }
233       }
234 
235       if (dest == null) {
236         dest = File.createTempFile("fsg", ".tmp");
237       } else if (dest.isDirectory()) {
238         throw new ProcessException("Destination file is a directory");
239       }
240       LOG.info("Template destination: " + dest);
241 
242       // Determine template contents
243       String template = null;
244 
245       Reference<?> refTemplate = service.getInput("template");
246       if (refTemplate == null) {
247         String path = service.getAttribute(NS_GROUNDING, "template-path");
248         if (path == null) {
249           template = service.getAttribute(NS_GROUNDING, "template");
250         } else {
251           File f = new File(path);
252           if (f.isFile()) {
253             template = FileUtils.readFileToString(f);
254           } else {
255             throw new ProcessException("Invalid template path: " + path);
256           }
257         }
258       } else {
259         if (refTemplate.canCastTo(String.class)) {
260           template = (String)refTemplate.castTo(String.class);
261         }
262       }
263 
264       if (template != null && template.length() > 0) {
265         String data = applyTemplate(template, service);
266         FileUtils.writeStringToFile(dest, data);
267       }
268 
269       Reference<?> output = service.getOutput("file");
270       if (output != null) {
271         if (output.canCastFrom(File.class)) {
272           output.setValue(dest);
273         } else {
274           LOG.warn("Invalid output type. Output ignored.");
275         }
276       }
277 
278 
279     } catch (IOException e) {
280       throw new ProcessException("I/O error", e);
281     }
282   }
283 
284   /**
285    * Applies variable replacements in the template, and returns the resulting
286    * text.
287    *
288    * @param template The template to process
289    * @param service  Service context
290    * @return Resulting text after all replacements
291    */
292   private String applyTemplate(String template, Service service) {
293     JXPathContext context = JXPathContext.newContext(service);
294 
295     String result = template;
296 
297     // DOTALL mode is necessary for crossing line boundaries in the template
298     Pattern p = Pattern.compile(".*\\{\\{(.+)\\}\\}.*", Pattern.DOTALL);
299 
300     boolean changed = true;
301     while (changed) {
302       changed = false;
303 
304       Matcher m = p.matcher(result);
305       if (m.matches()) {
306         String expr = m.group(1);
307         try {
308           Object value = context.getValue(expr);
309           if (value != null) {
310             changed = true;
311             result = result.replaceAll("\\{\\{" + expr + "\\}\\}", value.toString());
312           }
313         } catch (Exception e) {
314           e.printStackTrace();
315         }
316       }
317     }
318     return result;
319   }
320 
321   /**
322    * Retrieves a File instance from a reference.
323    *
324    * @param ref The source reference
325    * @return A File instance
326    * @throws ProcessException If the reference cannot be translated into a File
327    */
328   private File toFile(Reference<?> ref) throws ProcessException {
329     if (ref == null) {
330       return null;
331     } else if (ref.canCastTo(File.class)) {
332       return (File)ref.castTo(File.class);
333     } else if (ref.canCastTo(String.class)) {
334       return new File((String)ref.castTo(String.class));
335     } else {
336       throw new ProcessException("Invalid File reference");
337     }
338   }
339 
340   /**
341    * {@inheritDoc}
342    */
343   public void validate() throws ProcessException {
344   }
345 
346   /**
347    * {@inheritDoc}
348    */
349   public Grounding createGrounding() {
350     return this;
351   }
352 
353 }