001/*
002 * (C) Copyright 2015-2018 Nuxeo (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 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019package org.nuxeo.ecm.core.io.registry.context;
020
021import static org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter.ENTITY_TYPE;
022import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.EMBED_ENRICHERS;
023import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.EMBED_PROPERTIES;
024import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.FETCH_PROPERTIES;
025import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.HEADER_PREFIX;
026import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.SEPARATOR;
027import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.TRANSLATE_PROPERTIES;
028import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.WRAPPED_CONTEXT;
029
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Locale;
036import java.util.Map;
037import java.util.Set;
038import java.util.TreeSet;
039import java.util.concurrent.ConcurrentHashMap;
040import java.util.concurrent.CopyOnWriteArrayList;
041import java.util.function.Supplier;
042
043import org.apache.commons.collections.CollectionUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.nuxeo.ecm.core.api.CoreInstance;
046import org.nuxeo.ecm.core.api.CoreSession;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.io.registry.MarshallingConstants;
049import org.nuxeo.ecm.core.io.registry.MarshallingException;
050
051/**
052 * A thread-safe {@link RenderingContext} implementation. Please use {@link RenderingContext.CtxBuilder} to create
053 * instance of {@link RenderingContext}.
054 *
055 * @since 7.2
056 */
057public class RenderingContextImpl implements RenderingContext {
058
059    private String baseUrl = DEFAULT_URL;
060
061    private Locale locale = DEFAULT_LOCALE;
062
063    private CoreSession session = null;
064
065    private Supplier<SessionWrapper> sessionWrapperSupplier;
066
067    private final Map<String, List<Object>> parameters = new ConcurrentHashMap<>();
068
069    private RenderingContextImpl() {
070    }
071
072    @Override
073    public Locale getLocale() {
074        return locale;
075    }
076
077    @Override
078    public String getBaseUrl() {
079        return baseUrl;
080    }
081
082    @Override
083    public SessionWrapper getSession(DocumentModel document) {
084        if (document != null) {
085            CoreSession docSession = null;
086            try {
087                docSession = document.getCoreSession();
088            } catch (UnsupportedOperationException e) {
089                // do nothing
090            }
091            if (docSession != null) {
092                return new SessionWrapper(docSession, false);
093            }
094        }
095        if (session != null) {
096            return new SessionWrapper(session, false);
097        }
098
099        if (sessionWrapperSupplier != null) {
100            SessionWrapper sessionWrapper = sessionWrapperSupplier.get();
101            if (sessionWrapper != null) {
102                return sessionWrapper;
103            }
104        }
105
106        String repoNameFound = getParameter(REPOSITORY_NAME_REQUEST_HEADER);
107        if (StringUtils.isBlank(repoNameFound)) {
108            repoNameFound = getParameter(REPOSITORY_NAME_REQUEST_PARAMETER);
109            if (StringUtils.isBlank(repoNameFound) && document != null) {
110                try {
111                    repoNameFound = document.getRepositoryName();
112                } catch (UnsupportedOperationException e) {
113                    // do nothing
114                }
115            }
116        }
117        if (!StringUtils.isBlank(repoNameFound)) {
118            CoreSession repoSession = CoreInstance.openCoreSession(repoNameFound);
119            return new SessionWrapper(repoSession, true);
120        }
121        throw new MarshallingException("Unable to create a new session");
122    }
123
124    @Override
125    public void setExistingSession(CoreSession session) {
126        this.session = session;
127    }
128
129    @Override
130    public Set<String> getProperties() {
131        return getSplittedParameterValues(EMBED_PROPERTIES);
132    }
133
134    @Override
135    public Set<String> getFetched(String entity) {
136        return getSplittedParameterValues(FETCH_PROPERTIES, entity);
137    }
138
139    @Override
140    public Set<String> getTranslated(String entity) {
141        return getSplittedParameterValues(TRANSLATE_PROPERTIES, entity);
142    }
143
144    @Override
145    public Set<String> getEnrichers(String entity) {
146        return getSplittedParameterValues(EMBED_ENRICHERS, entity);
147    }
148
149    private Set<String> getSplittedParameterValues(String category, String... subCategories) {
150        // supports dot '.' as separator
151        Set<String> result = getSplittedParameterValues('.', category, subCategories);
152        // supports hyphen '-' as separator
153        result.addAll(getSplittedParameterValues(SEPARATOR, category, subCategories));
154        return result;
155    }
156
157    @SuppressWarnings("deprecation")
158    private Set<String> getSplittedParameterValues(char separator, String category, String... subCategories) {
159        if (category == null) {
160            return Collections.emptySet();
161        }
162        StringBuilder sb = new StringBuilder(category);
163        for (String subCategory : subCategories) {
164            sb.append(separator + subCategory);
165        }
166        String paramKey = sb.toString().toLowerCase();
167        List<Object> dirty = getParameters(paramKey);
168        dirty.addAll(getParameters(HEADER_PREFIX + paramKey));
169        // Deprecated on server since 5.8, but the code on client wasn't - keep this part of code as Nuxeo Automation
170        // Client is deprecated since 8.10 and Nuxeo Java Client handle this properly
171        // backward compatibility, supports X-NXDocumentProperties and X-NXContext-Category
172        if (EMBED_PROPERTIES.equalsIgnoreCase(paramKey)) {
173            dirty.addAll(getParameters("X-NXDocumentProperties"));
174        } else if ((EMBED_ENRICHERS + separator + ENTITY_TYPE).equalsIgnoreCase(paramKey)) {
175            dirty.addAll(getParameters("X-NXContext-Category"));
176        }
177        Set<String> result = new TreeSet<>();
178        for (Object value : dirty) {
179            if (value instanceof String) {
180                result.addAll(Arrays.asList(org.nuxeo.common.utils.StringUtils.split((String) value, ',', true)));
181            }
182        }
183        return result;
184    }
185
186    private <T> T getWrappedEntity(String name) {
187        return WrappedContext.getEntity(this, name);
188    }
189
190    @Override
191    public WrappedContext wrap() {
192        return WrappedContext.create(this);
193    }
194
195    @Override
196    public <T> T getParameter(String name) {
197        if (StringUtils.isEmpty(name)) {
198            return null;
199        }
200        String realName = name.toLowerCase().trim();
201        List<Object> values = parameters.get(realName);
202        if (CollectionUtils.isNotEmpty(values)) {
203            @SuppressWarnings("unchecked")
204            T value = (T) values.get(0);
205            return value;
206        }
207        if (WRAPPED_CONTEXT.equalsIgnoreCase(realName)) {
208            return null;
209        } else {
210            return getWrappedEntity(realName);
211        }
212    }
213
214    @Override
215    public boolean getBooleanParameter(String name) {
216        Object result = getParameter(name);
217        if (result == null) {
218            return false;
219        } else if (result instanceof Boolean) {
220            return (Boolean) result;
221        } else if (result instanceof String) {
222            try {
223                return Boolean.parseBoolean((String) result);
224            } catch (Exception e) {
225                return false;
226            }
227        }
228        return false;
229    }
230
231    @SuppressWarnings({ "rawtypes", "unchecked" })
232    @Override
233    public <T> List<T> getParameters(String name) {
234        if (StringUtils.isEmpty(name)) {
235            return null;
236        }
237        String realName = name.toLowerCase().trim();
238        List<T> values = (List<T>) parameters.get(realName);
239        List<T> result;
240        if (values != null) {
241            result = new ArrayList<>(values);
242        } else {
243            result = new ArrayList<>();
244        }
245        if (WRAPPED_CONTEXT.equalsIgnoreCase(realName)) {
246            return result;
247        } else {
248            Object wrapped = getWrappedEntity(realName);
249            if (wrapped == null) {
250                return result;
251            }
252            if (wrapped instanceof List) {
253                for (Object element : (List) wrapped) {
254                    try {
255                        T casted = (T) element;
256                        result.add(casted);
257                    } catch (ClassCastException e) {
258                        return null;
259                    }
260                }
261            } else {
262                try {
263                    T casted = (T) wrapped;
264                    result.add(casted);
265                } catch (ClassCastException e) {
266                    return null;
267                }
268            }
269        }
270        return result;
271    }
272
273    @Override
274    public Map<String, List<Object>> getAllParameters() {
275        // make a copy of the local parameters
276        Map<String, List<Object>> unModifiableParameters = new HashMap<>();
277        for (Map.Entry<String, List<Object>> entry : parameters.entrySet()) {
278            String key = entry.getKey();
279            List<Object> value = entry.getValue();
280            if (value == null) {
281                unModifiableParameters.put(key, null);
282            } else {
283                unModifiableParameters.put(key, new ArrayList<>(value));
284            }
285        }
286        return unModifiableParameters;
287    }
288
289    @Override
290    public void setParameterValues(String name, Object... values) {
291        if (StringUtils.isEmpty(name)) {
292            return;
293        }
294        String realName = name.toLowerCase().trim();
295        if (values.length == 0) {
296            parameters.remove(realName);
297            return;
298        }
299        setParameterListValues(realName, Arrays.asList(values));
300    }
301
302    @Override
303    public void setParameterListValues(String name, List<Object> values) {
304        if (StringUtils.isEmpty(name)) {
305            return;
306        }
307        String realName = name.toLowerCase().trim();
308        if (values == null) {
309            parameters.remove(realName);
310        } else {
311            parameters.put(realName, new CopyOnWriteArrayList<>(values));
312        }
313    }
314
315    @Override
316    public void addParameterValues(String name, Object... values) {
317        addParameterListValues(name, Arrays.asList(values));
318    }
319
320    @Override
321    public void addParameterListValues(String name, List<?> values) {
322        if (StringUtils.isEmpty(name)) {
323            return;
324        }
325        String realName = name.toLowerCase().trim();
326        if (values == null) {
327            return;
328        }
329        parameters.computeIfAbsent(realName, key -> new CopyOnWriteArrayList<>()).addAll(values);
330    }
331
332    static RenderingContextBuilder builder() {
333        return new RenderingContextBuilder();
334    }
335
336    public static final class RenderingContextBuilder {
337
338        private RenderingContextImpl ctx;
339
340        RenderingContextBuilder() {
341            ctx = new RenderingContextImpl();
342        }
343
344        public RenderingContextBuilder base(String url) {
345            ctx.baseUrl = url;
346            return this;
347        }
348
349        public RenderingContextBuilder locale(Locale locale) {
350            ctx.locale = locale;
351            return this;
352        }
353
354        public RenderingContextBuilder session(CoreSession session) {
355            ctx.session = session;
356            return this;
357        }
358
359        /**
360         * @since 9.3
361         */
362        public RenderingContextBuilder sessionWrapperSupplier(Supplier<SessionWrapper> supplier) {
363            ctx.sessionWrapperSupplier = supplier;
364            return this;
365        }
366
367        public RenderingContextBuilder param(String name, Object value) {
368            ctx.addParameterValues(name, value);
369            return this;
370        }
371
372        public RenderingContextBuilder paramValues(String name, Object... values) {
373            ctx.addParameterValues(name, values);
374            return this;
375        }
376
377        public RenderingContextBuilder paramList(String name, List<?> values) {
378            ctx.addParameterListValues(name, values);
379            return this;
380        }
381
382        public RenderingContextBuilder properties(String... schemaName) {
383            return paramValues(EMBED_PROPERTIES, (Object[]) schemaName);
384        }
385
386        public RenderingContextBuilder enrich(String entityType, String... enricherName) {
387            return paramValues(EMBED_ENRICHERS + SEPARATOR + entityType, (Object[]) enricherName);
388        }
389
390        public RenderingContextBuilder enrichDoc(String... enricherName) {
391            return enrich(ENTITY_TYPE, enricherName);
392        }
393
394        public RenderingContextBuilder fetch(String entityType, String... propertyName) {
395            return paramValues(FETCH_PROPERTIES + SEPARATOR + entityType, (Object[]) propertyName);
396        }
397
398        public RenderingContextBuilder fetchInDoc(String... propertyName) {
399            return fetch(ENTITY_TYPE, propertyName);
400        }
401
402        public RenderingContextBuilder translate(String entityType, String... propertyName) {
403            return paramValues(TRANSLATE_PROPERTIES + SEPARATOR + entityType, (Object[]) propertyName);
404        }
405
406        public RenderingContextBuilder depth(DepthValues value) {
407            ctx.setParameterValues(MarshallingConstants.MAX_DEPTH_PARAM, value.name());
408            return this;
409        }
410
411        public RenderingContext get() {
412            return ctx;
413        }
414
415    }
416
417}