001/*
002 * (C) Copyright 2006-2017 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 *     bstefanescu
018 */
019package org.nuxeo.ecm.webengine.app;
020
021import java.io.BufferedReader;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.lang.reflect.Type;
027import java.net.URL;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashSet;
032import java.util.Map;
033import java.util.Set;
034
035import javax.servlet.http.HttpServletRequest;
036import javax.servlet.http.HttpServletResponse;
037import javax.ws.rs.Path;
038import javax.ws.rs.core.Application;
039import javax.ws.rs.core.Context;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.platform.web.common.RequestContext;
044import org.nuxeo.ecm.webengine.WebEngine;
045import org.nuxeo.ecm.webengine.jaxrs.ApplicationFactory;
046import org.nuxeo.ecm.webengine.jaxrs.scan.Scanner;
047import org.nuxeo.ecm.webengine.loader.WebLoader;
048import org.nuxeo.ecm.webengine.model.Module;
049import org.nuxeo.ecm.webengine.model.WebContext;
050import org.nuxeo.ecm.webengine.model.WebObject;
051import org.nuxeo.ecm.webengine.model.impl.DefaultTypeLoader;
052import org.nuxeo.ecm.webengine.model.impl.ModuleConfiguration;
053import org.nuxeo.ecm.webengine.model.impl.ModuleManager;
054import org.osgi.framework.Bundle;
055
056import com.sun.jersey.core.spi.component.ComponentContext;
057import com.sun.jersey.core.spi.component.ComponentScope;
058import com.sun.jersey.server.impl.inject.ServerInjectableProviderContext;
059import com.sun.jersey.spi.inject.Injectable;
060import com.sun.jersey.spi.inject.InjectableProvider;
061
062/**
063 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
064 */
065public class WebEngineModule extends Application implements ApplicationFactory {
066
067    private final static Log log = LogFactory.getLog(WebEngineModule.class);
068
069    public final static String WEBOBJECT_ANNO = "Lorg/nuxeo/ecm/webengine/model/WebObject;";
070
071    public final static String WEBADAPTER_ANNO = "Lorg/nuxeo/ecm/webengine/model/WebAdapter;";
072
073    protected ServerInjectableProviderContext context;
074
075    protected Bundle bundle;
076
077    protected ModuleConfiguration cfg;
078
079    void init(WebEngine engine, Bundle bundle, File configuration, Map<String, String> attrs)
080            throws ReflectiveOperationException, IOException {
081        this.bundle = bundle;
082        loadModuleConfigurationFile(engine, configuration);
083        if (attrs != null) {
084            String v = attrs.get("name");
085            if (v != null) {
086                cfg.name = v;
087            }
088            v = attrs.get("extends");
089            if (v != null) {
090                cfg.base = v;
091            }
092            v = attrs.get("headless");
093            if (v != null) {
094                cfg.isHeadless = Boolean.parseBoolean(v);
095            }
096        }
097        if (cfg.name == null) {
098            throw new IllegalStateException("No name given for web module in bundle " + bundle.getSymbolicName());
099        }
100        initTypes(bundle, attrs.get("package"), engine);
101    }
102
103    private void initTypes(Bundle bundle, String packageBase, WebEngine engine)
104            throws ReflectiveOperationException, IOException {
105        cfg.types = getWebTypes();
106        if (cfg.types == null) {
107            // try the META-INF/web-types file
108            loadMetaTypeFile(engine);
109            if (cfg.types == null) {
110                // try scanning the bundle
111                scan(bundle, packageBase);
112                if (cfg.types == null) {
113                    throw new IllegalStateException("No web types defined in web module " + cfg.name + " from bundle "
114                            + bundle.getSymbolicName());
115                }
116            } else {
117                initRoots(engine);
118            }
119        } else {
120            initRoots(engine);
121        }
122    }
123
124    private void scan(Bundle bundle, String packageBase) throws ReflectiveOperationException, IOException {
125        if (packageBase == null) {
126            packageBase = "/";
127        }
128        Scanner scanner = new Scanner(bundle, packageBase, Scanner.PATH_ANNO, Scanner.PROVIDER_ANNO, WEBOBJECT_ANNO,
129                WEBADAPTER_ANNO);
130        scanner.scan();
131        Collection<Class<?>> paths = scanner.getCollector(Scanner.PATH_ANNO);
132        Collection<Class<?>> providers = scanner.getCollector(Scanner.PROVIDER_ANNO);
133        cfg.roots = new Class<?>[paths.size() + providers.size()];
134        int i = 0;
135        for (Class<?> cl : paths) {
136            cfg.roots[i++] = cl;
137        }
138        for (Class<?> cl : providers) {
139            cfg.roots[i++] = cl;
140        }
141        Collection<Class<?>> objs = scanner.getCollector(WEBOBJECT_ANNO);
142        Collection<Class<?>> adapters = scanner.getCollector(WEBADAPTER_ANNO);
143        cfg.types = new Class<?>[objs.size() + adapters.size()];
144        i = 0;
145        for (Class<?> cl : objs) {
146            cfg.types[i++] = cl;
147        }
148        for (Class<?> cl : adapters) {
149            cfg.types[i++] = cl;
150        }
151    }
152
153    private void loadMetaTypeFile(WebEngine engine) throws ReflectiveOperationException, IOException {
154        URL url = bundle.getEntry(DefaultTypeLoader.WEB_TYPES_FILE);
155        if (url != null) {
156            try (InputStream in = url.openStream()) {
157                cfg.types = readWebTypes(engine.getWebLoader(), in);
158            }
159        }
160    }
161
162    private static Class<?>[] readWebTypes(WebLoader loader, InputStream in)
163            throws ReflectiveOperationException, IOException {
164        HashSet<Class<?>> types = new HashSet<>();
165        BufferedReader reader = null;
166        try {
167            reader = new BufferedReader(new InputStreamReader(in));
168            String line;
169            while ((line = reader.readLine()) != null) {
170                line = line.trim();
171                if (line.length() == 0 || line.startsWith("#")) {
172                    continue;
173                }
174                int p = line.indexOf('|');
175                if (p > -1) {
176                    line = line.substring(0, p);
177                }
178                Class<?> cl = loader.loadClass(line);
179                types.add(cl);
180            }
181        } finally {
182            if (reader != null) {
183                try {
184                    reader.close();
185                } catch (IOException e) {
186                }
187            }
188        }
189        return types.toArray(new Class<?>[types.size()]);
190    }
191
192    private void initRoots(WebEngine engine) {
193        ArrayList<Class<?>> roots = new ArrayList<>();
194        for (Class<?> cl : cfg.types) {
195            if (cl.isAnnotationPresent(Path.class)) {
196                roots.add(cl);
197            } else if (cfg.rootType != null) {
198                // compat mode - should be removed later
199                WebObject wo = cl.getAnnotation(WebObject.class);
200                if (wo != null && wo.type().equals(cfg.rootType)) {
201                    log.warn("Invalid web module " + cfg.name + " from bundle " + bundle.getSymbolicName()
202                            + ". The root-type " + cl
203                            + " in module.xml is deprecated. Consider using @Path annotation on you root web objects.");
204                }
205            }
206        }
207        if (roots.isEmpty()) {
208            log.error(
209                    "No root web objects found in web module " + cfg.name + " from bundle " + bundle.getSymbolicName());
210            // throw new
211            // IllegalStateException("No root web objects found in web module "+cfg.name+" from bundle
212            // "+bundle.getSymbolicName());
213        }
214        cfg.roots = roots.toArray(new Class<?>[roots.size()]);
215    }
216
217    private ModuleConfiguration loadModuleConfigurationFile(WebEngine engine, File file) throws IOException {
218        if (file != null && file.isFile()) {
219            cfg = ModuleManager.readConfiguration(engine, file);
220        } else {
221            cfg = new ModuleConfiguration();
222        }
223        cfg.engine = engine;
224        cfg.file = file;
225        return cfg;
226    }
227
228    public ModuleConfiguration getConfiguration() {
229        return cfg;
230    }
231
232    public Module getModule(WebContext context) {
233        return cfg.get(context);
234    }
235
236    public Bundle getBundle() {
237        return bundle;
238    }
239
240    @Override
241    public Set<Class<?>> getClasses() {
242        if (cfg.roots == null) {
243            return new HashSet<>();
244        }
245        Set<Class<?>> set = new HashSet<>();
246        Collections.addAll(set, cfg.roots);
247        return set;
248    }
249
250    @Override
251    public Set<Object> getSingletons() {
252        Set<Object> set = new HashSet<>();
253        set.add(new HttpServletRequestProvider());
254        set.add(new HttpServletResponseProvider());
255        return set;
256    }
257
258    public String getId() {
259        return bundle.getSymbolicName();
260    }
261
262    public Class<?>[] getWebTypes() {
263        return null; // types will be discovered
264    }
265
266    @Override
267    public Application getApplication(Bundle bundle, Map<String, String> args)
268            throws ReflectiveOperationException, IOException {
269        return WebEngineModuleFactory.getApplication(this, bundle, args);
270    }
271
272    public static class HttpServletRequestProvider
273            implements InjectableProvider<Context, Type>, Injectable<HttpServletRequest> {
274        @Override
275        public HttpServletRequest getValue() {
276            RequestContext ctx = RequestContext.getActiveContext();
277            return ctx != null ? ctx.getRequest() : null;
278        }
279
280        @Override
281        public ComponentScope getScope() {
282            return ComponentScope.PerRequest;
283        }
284
285        @Override
286        public Injectable<HttpServletRequest> getInjectable(ComponentContext ic, Context a, Type c) {
287            if (!c.equals(HttpServletRequest.class)) {
288                return null;
289            }
290            return this;
291        }
292
293    }
294
295    public static class HttpServletResponseProvider
296            implements InjectableProvider<Context, Type>, Injectable<HttpServletResponse> {
297        @Override
298        public HttpServletResponse getValue() {
299            RequestContext ctx = RequestContext.getActiveContext();
300            return ctx != null ? ctx.getResponse() : null;
301        }
302
303        @Override
304        public ComponentScope getScope() {
305            return ComponentScope.PerRequest;
306        }
307
308        @Override
309        public Injectable<HttpServletResponse> getInjectable(ComponentContext ic, Context a, Type c) {
310            if (!c.equals(HttpServletResponse.class)) {
311                return null;
312            }
313            return this;
314        }
315
316    }
317
318}