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