001// Copyright 2014 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.corelib.pages;
016
017import java.util.ArrayList;
018import java.util.Collections;
019import java.util.Comparator;
020import java.util.List;
021
022import org.apache.tapestry5.Block;
023import org.apache.tapestry5.annotations.Cached;
024import org.apache.tapestry5.annotations.InjectComponent;
025import org.apache.tapestry5.annotations.OnEvent;
026import org.apache.tapestry5.annotations.Persist;
027import org.apache.tapestry5.annotations.Property;
028import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
029import org.apache.tapestry5.annotations.WhitelistAccessOnly;
030import org.apache.tapestry5.corelib.components.Zone;
031import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
032import org.apache.tapestry5.internal.ThrowawayClassLoader;
033import org.apache.tapestry5.internal.plastic.ClassLoaderDelegate;
034import org.apache.tapestry5.internal.plastic.PlasticClassLoader;
035import org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGenerator;
036import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
037import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
038import org.apache.tapestry5.ioc.annotations.Description;
039import org.apache.tapestry5.ioc.annotations.Inject;
040import org.apache.tapestry5.ioc.annotations.Symbol;
041import org.apache.tapestry5.json.JSONArray;
042import org.apache.tapestry5.json.JSONObject;
043import org.apache.tapestry5.services.ComponentClassResolver;
044import org.apache.tapestry5.services.ComponentLibraryInfo;
045import org.apache.tapestry5.services.ComponentLibraryInfoSource;
046import org.apache.tapestry5.services.LibraryMapping;
047import org.apache.tapestry5.util.TextStreamResponse;
048
049/**
050 * Page used to describe the component libraries being used in the application.
051 * Notice: the implementation of this page was done to avoid creating components, so the
052 * Tapestry 5 Core Library didn't get polluted with internal-only components.
053 */
054@UnknownActivationContextCheck(false)
055@WhitelistAccessOnly
056public class ComponentLibraries
057{
058
059    final private static String[] EMTPY_STRING_ARRAY = {};
060
061    private static final Comparator<LibraryMapping> LIBRARY_MAPPING_COMPARATOR = new Comparator<LibraryMapping>()
062    {
063        @Override
064        public int compare(LibraryMapping mapping1, LibraryMapping mapping2)
065        {
066            return mapping1.libraryName.compareTo(mapping2.libraryName);
067        }
068    };
069
070    private static enum Type { PAGE, COMPONENT, MIXIN }
071    
072    @InjectComponent
073    private Zone zone;
074
075    @Inject
076    private ComponentClassResolver componentClassResolver;
077
078    @Property
079    private LibraryMapping libraryMapping;
080
081    @Property
082    private String logicalName;
083    
084    @Property
085    private List<String> logicalNames;
086
087    @Property
088    private String headerName;
089
090    @Property
091    private List<String> pages;
092    
093    @Property
094    private List<String> components;
095    
096    @Property
097    private List<String> mixins;
098    
099    private Type type; 
100    
101    @Inject
102    private Block classesTable;
103    
104    @Inject
105    @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
106    @Property
107    private boolean productionMode;
108    
109    @Inject
110    private ComponentLibraryInfoSource componentLibraryInfoSource;
111    
112    @Inject
113    private ComponentDependencyRegistry componentDependencyRegistry;
114    
115    @Inject
116    private ComponentDependencyGraphvizGenerator componentDependencyGraphvizGenerator;
117    
118    @Property
119    private String selectedComponent;
120    
121    @Property
122    private String dependency;
123    
124    @Persist
125    @Property
126    private boolean showEverything;
127    
128//    void onActivate(List<String> context)
129//    {
130//        if (context.size() > 0)
131//        {
132//            selectedComponent = String.join("/", context);
133//        }
134//        else
135//        {
136//            selectedComponent = null;
137//        }
138//    }
139//    
140//    Object[] onPassivate()
141//    {
142//        return selectedComponent.split("/");
143//    }
144    
145    @Cached
146    public List<LibraryMapping> getLibraryMappings()
147    {
148        List<LibraryMapping> mappings = new ArrayList<LibraryMapping>();
149        
150        // add all the library mappings, except the "" (empty string) one.
151        for (LibraryMapping libraryMapping : componentClassResolver.getLibraryMappings())
152        {
153            if (showEverything || !"".equals(libraryMapping.libraryName)) {
154                mappings.add(libraryMapping);
155            }
156        }
157        
158        Collections.sort(mappings, LIBRARY_MAPPING_COMPARATOR);
159        return mappings;
160    }
161    
162    @Cached(watch="libraryMapping")
163    public ComponentLibraryInfo getInfo()
164    {
165        return componentLibraryInfoSource.find(libraryMapping);
166    }
167    
168    public List<String> getLibraryNames() {
169        return componentClassResolver.getLibraryNames();
170    }
171    
172    public String getLibraryClientId() 
173    {
174        return libraryMapping.libraryName.replace("/", "-");
175    }
176
177    private List<String> filter(final List<String> allNames)
178    {
179        List<String> logicalNames = new ArrayList<String>();
180        final List<LibraryMapping> libraryMappings = getLibraryMappings();
181        for (String name : allNames)
182        {
183            
184            if (name.startsWith(libraryMapping.libraryName + "/") && 
185                    !(libraryMapping.libraryName.equals("core") && name.endsWith("Test")))
186            {
187                logicalNames.add(name);
188            }
189            else
190            {
191                if (libraryMapping.libraryName.equals(""))
192                {
193                    boolean isWebappLibrary = true;
194                    for (LibraryMapping otherLibraryMapping : libraryMappings) {
195                        if (!libraryMapping.equals(otherLibraryMapping) &&
196                                name.startsWith(otherLibraryMapping.libraryName + "/"))
197                        {
198                            isWebappLibrary = false;
199                            break;
200                        }
201                    }
202                    if (isWebappLibrary)
203                    {
204                        logicalNames.add(name);
205                    }
206                }
207            }
208        }
209        
210        return logicalNames;
211    }
212    
213    public Block getComponentsTable()
214    {
215        logicalNames = filter(componentClassResolver.getComponentNames());
216        type = Type.COMPONENT;
217        headerName = "Components";
218        return classesTable;
219    }
220    
221    public Block getPagesTable()
222    {
223        logicalNames = filter(componentClassResolver.getPageNames());
224        type = Type.PAGE;
225        headerName = "Pages";
226        return classesTable;
227    }
228
229    public Block getMixinsTable()
230    {
231        logicalNames = filter(componentClassResolver.getMixinNames());
232        type = Type.MIXIN;
233        headerName = "Mixins";
234        return classesTable;
235    }
236    
237    public String getSourceUrl()
238    {
239        return getInfo() != null ? getInfo().getSourceUrl(getClassName()) : null;
240    }
241    
242    public String getJavaDocUrl() 
243    {
244        return getInfo() != null ? getInfo().getJavadocUrl(getClassName()) : null;
245    }
246
247    private String getClassName()
248    {
249        return getClassName(logicalName, type, componentClassResolver);
250    }
251    
252    private static String getClassName(String logicalName, Type type, ComponentClassResolver componentClassResolver)
253    {
254        String className;
255        switch (type)
256        {
257            case PAGE: className = componentClassResolver.resolvePageNameToClassName(logicalName); break;
258            case COMPONENT: className = componentClassResolver.resolveComponentTypeToClassName(logicalName); break;
259            case MIXIN: className = componentClassResolver.resolveMixinTypeToClassName(logicalName); break;
260            default: className = null; // should never happen
261        }
262        return className;
263    }
264    
265    public String getSimpleLogicalName()
266    {
267        return logicalName.replace("core/", "");
268    }
269    
270    @Cached(watch = "logicalName")
271    public String[] getTags() throws ClassNotFoundException {
272        Description description = getDescription();
273        return description != null ? description.tags() : EMTPY_STRING_ARRAY;
274    }
275
276    @Cached(watch = "logicalName")
277    public Description getDescription() throws ClassNotFoundException
278    {
279        try {
280            return Class.forName(getClassName()).getAnnotation(Description.class);
281        } catch (Exception e) {
282            e.printStackTrace();
283            return null;
284        }
285    }
286
287    public boolean isClassHasTags() throws ClassNotFoundException
288    {
289        return getTags().length > 0;
290    }
291    
292    @OnEvent("json")
293    Object generateJSONDescription(String libraryName)
294    {
295        for (LibraryMapping mapping : componentClassResolver.getLibraryMappings())
296        {
297            if (libraryName.equalsIgnoreCase(mapping.libraryName))
298            {
299                this.libraryMapping = mapping;
300                break;
301            }
302        }
303        JSONObject object = new JSONObject();
304        object.put("libraryName", libraryName);
305        object.put("rootPackage", libraryMapping.getRootPackage());
306        
307        final ComponentLibraryInfo info = getInfo();
308        if (info != null)
309        {
310            JSONObject infoJsonObject = new JSONObject();
311            putIfNotNull("description", info.getDescription(), infoJsonObject);
312            putIfNotNull("homepage", info.getHomepageUrl(), infoJsonObject);
313            putIfNotNull("documentationUrl", info.getDocumentationUrl(), infoJsonObject);
314            putIfNotNull("javadocUrl", info.getJavadocUrl(), infoJsonObject);
315            putIfNotNull("groupId", info.getGroupId(), infoJsonObject);
316            putIfNotNull("artifactId", info.getArtifactId(), infoJsonObject);
317            putIfNotNull("version", info.getVersion(), infoJsonObject);
318            putIfNotNull("sourceBrowseUrl", info.getSourceBrowseUrl(), infoJsonObject);
319            putIfNotNull("sourceRootUrl", info.getSourceRootUrl(), infoJsonObject);
320            putIfNotNull("issueTrackerUrl", info.getIssueTrackerUrl(), infoJsonObject);
321            putIfNotNull("dependencyInfoUrl", info.getDependencyManagementInfoUrl(), infoJsonObject);
322            
323            if (info.getTags() != null)
324            {
325                for (String tag : info.getTags())
326                {
327                    infoJsonObject.accumulate("tags", tag);
328                }
329            }
330            
331            object.put("info", infoJsonObject);
332            
333        }
334        
335        addClasses("components", filter(componentClassResolver.getComponentNames()), Type.COMPONENT, info, object);
336        addClasses("pages", filter(componentClassResolver.getPageNames()), Type.PAGE, info, object);
337        addClasses("mixins", filter(componentClassResolver.getMixinNames()), Type.MIXIN, info, object);
338        
339        return new TextStreamResponse("text/javascript", object.toString());
340        
341    }
342
343    private void addClasses(final String property, List<String> classes, Type type,
344            final ComponentLibraryInfo info, JSONObject object)
345    {
346        if (classes.size() > 0)
347        {
348            JSONArray classesJsonArray = new JSONArray();
349            for (String logicalName : classes)
350            {
351                logicalName = logicalName.replace("core/", "");
352                final String className = getClassName(logicalName, type, componentClassResolver);
353                JSONObject classJsonObject = new JSONObject();
354                classJsonObject.put("logicalName", logicalName);
355                classJsonObject.put("class", className);
356                if (info != null)
357                {
358                    putIfNotNull("sourceUrl", info.getSourceUrl(className), classJsonObject);
359                    putIfNotNull("javadocUrl", info.getJavadocUrl(className), classJsonObject);
360                }
361                try
362                {
363                    final Description description = getClass(className);
364                    if (description != null)
365                    {
366                        putIfNotNull("description", description.text(), classJsonObject);
367                        if (description.tags().length > 0)
368                        {
369                            for (String tag : description.tags())
370                            {
371                                classJsonObject.accumulate("tag", tag);
372                            }
373                        }
374                    }
375                }
376                catch (ClassNotFoundException e)
377                {
378                    throw new RuntimeException(e);
379                }
380                classesJsonArray.put(classJsonObject);
381            }
382            object.put(property, classesJsonArray);
383        }
384    }
385
386    private Description getClass(final String className) throws ClassNotFoundException
387    {
388        return Class.forName(className).getAnnotation(Description.class);
389    }
390    
391    private void putIfNotNull(String propertyName, String value, JSONObject object)
392    {
393        if (value != null)
394        {
395            object.put(propertyName, value);
396        }
397    }
398    
399    public String getGraphvizValue()
400    {
401        return componentDependencyGraphvizGenerator.generate(
402                getClassName(selectedComponent));
403    }
404    
405    public String getClassName(String logicalName)
406    {
407        return componentClassResolver.getClassName(logicalName);
408    }
409    
410    public String getComponentClassName()
411    {
412        return getClassName(selectedComponent);
413    }
414    
415    public List<String> getDependencies()
416    {
417        final String className = componentClassResolver.getClassName(selectedComponent);
418        final List<String> dependencies = new ArrayList<>();
419        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE));
420        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS));
421        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE));
422        Collections.sort(dependencies);
423        return dependencies;
424    }
425    
426    public List<String> getDependents()
427    {
428        final String className = componentClassResolver.getClassName(selectedComponent);
429        List<String> dependents = new ArrayList<>(
430                componentDependencyRegistry.getDependents(className));
431        Collections.sort(dependents);
432        return dependents;
433    }
434    
435    public String getDisplayLogicalName()
436    {
437        return componentClassResolver.getLogicalName(dependency);
438    }
439    
440    public Object onSelectComponent(String selectedComponent)
441    {
442        this.selectedComponent = selectedComponent;
443        final String className = componentClassResolver.getClassName(selectedComponent);
444        if (!componentDependencyRegistry.contains(className)) 
445        {
446            
447            final ClassLoader classLoader = new ThrowawayClassLoader(getClass().getClassLoader());
448            
449            try 
450            {
451                componentDependencyRegistry.register(classLoader.loadClass(className));
452            } catch (ClassNotFoundException e) 
453            {
454                throw new RuntimeException(e);
455            }
456        }
457        return zone.getBody();
458    }
459    
460    public Object getContext()
461    {
462        return logicalName;
463    }
464    
465    public Object onReset()
466    {
467        selectedComponent = null;
468        return zone.getBody();
469    }
470    
471    public Object onShowEverything()
472    {
473        showEverything = true;
474        return zone.getBody();
475    }
476    
477    public Object onShowRestricted()
478    {
479        showEverything = false;
480        return zone.getBody();
481    }
482    
483    public String getLibraryName()
484    {
485        return !libraryMapping.libraryName.isEmpty() ? libraryMapping.libraryName : "Webapp's own component library";
486    }
487
488}