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