001/*
002 * (C) Copyright 2012-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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 * Contributors:
017 *     bstefanescu
018 *     vpasquier <vpasquier@nuxeo.com>
019 *     slacoin <slacoin@nuxeo.com>
020 */
021package org.nuxeo.ecm.automation.core;
022
023import static org.nuxeo.ecm.automation.core.Constants.T_BOOLEAN;
024import static org.nuxeo.ecm.automation.core.Constants.T_DATE;
025import static org.nuxeo.ecm.automation.core.Constants.T_DOCUMENT;
026import static org.nuxeo.ecm.automation.core.Constants.T_DOCUMENTS;
027import static org.nuxeo.ecm.automation.core.Constants.T_FLOAT;
028import static org.nuxeo.ecm.automation.core.Constants.T_INTEGER;
029import static org.nuxeo.ecm.automation.core.Constants.T_LONG;
030import static org.nuxeo.ecm.automation.core.Constants.T_PROPERTIES;
031import static org.nuxeo.ecm.automation.core.Constants.T_RESOURCE;
032import static org.nuxeo.ecm.automation.core.Constants.T_STRING;
033
034import java.io.IOException;
035import java.net.MalformedURLException;
036import java.net.URL;
037import java.util.ArrayList;
038import java.util.HashMap;
039import java.util.List;
040import java.util.Map;
041
042import org.apache.commons.lang.StringEscapeUtils;
043import org.nuxeo.common.utils.StringUtils;
044import org.nuxeo.common.xmap.annotation.XContent;
045import org.nuxeo.common.xmap.annotation.XNode;
046import org.nuxeo.common.xmap.annotation.XNodeList;
047import org.nuxeo.common.xmap.annotation.XNodeMap;
048import org.nuxeo.common.xmap.annotation.XObject;
049import org.nuxeo.ecm.automation.OperationChain;
050import org.nuxeo.ecm.automation.OperationDocumentation;
051import org.nuxeo.ecm.automation.OperationException;
052import org.nuxeo.ecm.automation.OperationParameters;
053import org.nuxeo.ecm.automation.core.impl.adapters.helper.TypeAdapterHelper;
054import org.nuxeo.ecm.automation.core.scripting.Scripting;
055import org.nuxeo.ecm.automation.core.util.Properties;
056import org.nuxeo.ecm.core.api.impl.DocumentRefListImpl;
057import org.nuxeo.ecm.core.schema.utils.DateParser;
058import org.osgi.framework.Bundle;
059
060/**
061 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
062 */
063@XObject("chain")
064public class OperationChainContribution {
065
066    @XNode("@id")
067    protected String id;
068
069    @XNode("@replace")
070    protected boolean replace = true;
071
072    @XNode("description")
073    protected String description;
074
075    @XNodeList(value = "operation", type = Operation[].class, componentType = Operation.class)
076    protected Operation[] ops = new Operation[0];
077
078    @XNode("public")
079    protected boolean isPublic = true;
080
081    @XNodeList(value = "param", type = OperationDocumentation.Param[].class, componentType = OperationDocumentation.Param.class)
082    protected OperationDocumentation.Param[] params = new OperationDocumentation.Param[0];
083
084    /**
085     * @since 7.1
086     */
087    @XNodeList(value = "aliases/alias", type = String[].class, componentType = String.class)
088    protected String[] aliases;
089
090    @XObject("operation")
091    public static class Operation {
092        @XNode("@id")
093        protected String id;
094
095        @XNodeList(value = "param", type = ArrayList.class, componentType = Param.class)
096        protected List<Param> params = new ArrayList<>();
097
098        public String getId() {
099            return id;
100        }
101
102        public List<Param> getParams() {
103            return params;
104        }
105    }
106
107    @XObject("param")
108    public static class Param {
109        @XNode("@name")
110        protected String name;
111
112        // string, boolean, date, integer, float, uid, path, expression,
113        // template, resource
114        @XNode("@type")
115        protected String type = "string";
116
117        // why not XNode here? XContent requires to unescape XML entities, see
118        // below
119        @XContent
120        protected String value;
121
122        // Optional map for properties type values
123        @XNodeMap(value = "property", key = "@key", type = HashMap.class, componentType = String.class, nullByDefault = true)
124        protected Map<String, String> map;
125
126        public String getName() {
127            return name;
128        }
129
130        public String getValue() {
131            return value;
132        }
133
134        public String getType() {
135            return type;
136        }
137
138        public Map<String, String> getMap() {
139            return map;
140        }
141    }
142
143    public OperationDocumentation.Param[] getParams() {
144        return params;
145    }
146
147    public String getId() {
148        return id;
149    }
150
151    public OperationChain toOperationChain(Bundle bundle) throws OperationException {
152        OperationChain chain = new OperationChain(id);
153        chain.setDescription(description);
154        chain.setPublic(isPublic);
155        chain.setAliases(aliases);
156        for (Operation op : ops) {
157            OperationParameters params = chain.add(op.id);
158            for (Param param : op.params) {
159                param.value = param.value.trim();
160                // decode XML entities in every case
161                param.value = StringEscapeUtils.unescapeXml(param.value);
162                if (param.value.startsWith("expr:")) {
163                    String value = param.value.substring(5);
164                    if (value.contains("@{")) {
165                        params.set(param.name, Scripting.newTemplate(value));
166                    } else {
167                        params.set(param.name, Scripting.newExpression(value));
168                    }
169                } else {
170                    Object val = null;
171                    String type = param.type.toLowerCase();
172                    char c = type.charAt(0);
173                    switch (c) {
174                    case 's': // string
175                        if (T_STRING.equals(type)) {
176                            val = param.value;
177                        }
178                        break;
179                    case 'p':
180                        if (T_PROPERTIES.equals(type)) {
181                            if (param.map != null && !param.map.isEmpty()) {
182                                val = new Properties(param.map);
183                            } else {
184                                try {
185                                    val = new Properties(param.value);
186                                } catch (IOException e) {
187                                    throw new OperationException(e);
188                                }
189                            }
190                        }
191                        break;
192                    case 'i':
193                        if (T_INTEGER.equals(type)) {
194                            val = Integer.parseInt(param.value);
195                        }
196                        break;
197                    case 'l':
198                        if (T_LONG.equals(type)) {
199                            val = Long.valueOf(param.value);
200                        }
201                        break;
202                    case 'b':
203                        if (T_BOOLEAN.equals(type)) {
204                            val = Boolean.valueOf(param.value);
205                        }
206                        break;
207                    case 'd':
208                        if (T_DOCUMENT.equals(type)) {
209                            val = TypeAdapterHelper.createDocumentRefOrExpression(param.value);
210                        } else if (T_DOCUMENTS.equals(type)) {
211                            String[] ar = StringUtils.split(param.value, ',', true);
212                            DocumentRefListImpl result = new DocumentRefListImpl(ar.length);
213                            for (String ref : ar) {
214                                result.add(TypeAdapterHelper.createDocumentRef(ref));
215                            }
216                            val = result;
217                        } else if (T_DATE.equals(type)) {
218                            val = DateParser.parseW3CDateTime(param.value);
219                        }
220                        break;
221                    case 'f':
222                        if (T_FLOAT.equals(type)) {
223                            val = Double.valueOf(param.value);
224                        }
225                        break;
226                    case 'r':
227                        if (T_RESOURCE.equals(type)) {
228                            if (param.value.contains(":/")) { // a real URL
229                                try {
230                                    val = new URL(param.value);
231                                } catch (MalformedURLException e) {
232                                    throw new OperationException(e);
233                                }
234                            } else { // try with class loader
235                                val = bundle.getEntry(param.value);
236                            }
237                        }
238                        break;
239                    }
240                    if (val == null) {
241                        val = param.value;
242                    }
243                    params.set(param.name, val);
244                }
245            }
246        }
247        return chain;
248    }
249
250    public Operation[] getOps() {
251        return ops;
252    }
253
254    public String getLabel() {
255        return id;
256    }
257
258    public String getRequires() {
259        return "";
260    }
261
262    public String getCategory() {
263        return Constants.CAT_CHAIN;
264    }
265
266    public String getSince() {
267        return "";
268    }
269
270    public String getDescription() {
271        return description;
272    }
273
274    public String[] getAliases() {
275        return aliases;
276    }
277
278    public static OperationChainContribution contribOf(OperationChain chain, boolean replace) {
279        OperationChainContribution contrib = new OperationChainContribution();
280        contrib.id = chain.getId();
281        contrib.aliases = chain.getAliases();
282        contrib.description = "inlined chain of " + contrib.id;
283        contrib.isPublic = false;
284        contrib.params = paramsOf(chain.getChainParameters());
285        contrib.ops = operationsOf(chain.getOperations());
286        contrib.replace = replace;
287        return contrib;
288    }
289
290    public static OperationDocumentation.Param[] paramsOf(Map<String, ?> args) {
291        return args.entrySet().stream().map(entry -> {
292            OperationDocumentation.Param param = new OperationDocumentation.Param();
293            param.name = entry.getKey();
294            param.type = entry.getClass().getName();
295            return param;
296        }).toArray(OperationDocumentation.Param[]::new);
297    }
298
299    public static Operation[] operationsOf(List<OperationParameters> operations) {
300        return operations.stream().map(params -> {
301            Operation op = new Operation();
302            op.id = params.id();
303            params.map().forEach((k,v) -> {
304                Param param = new Param();
305                param.name = k;
306                param.type = "unknown";
307                param.value = v == null ? "null" : v.toString();
308                op.params.add(param);
309            });
310            return op;
311        }).toArray(Operation[]::new);
312    }
313}