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