001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.transform;
014
015import org.apache.tapestry5.Asset;
016import org.apache.tapestry5.ComponentResources;
017import org.apache.tapestry5.SymbolConstants;
018import org.apache.tapestry5.annotations.Import;
019import org.apache.tapestry5.annotations.SetupRender;
020import org.apache.tapestry5.func.F;
021import org.apache.tapestry5.func.Mapper;
022import org.apache.tapestry5.func.Worker;
023import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
024import org.apache.tapestry5.ioc.services.SymbolSource;
025import org.apache.tapestry5.model.MutableComponentModel;
026import org.apache.tapestry5.plastic.*;
027import org.apache.tapestry5.plastic.PlasticUtils.FieldInfo;
028import org.apache.tapestry5.services.AssetSource;
029import org.apache.tapestry5.services.TransformConstants;
030import org.apache.tapestry5.services.javascript.Initialization;
031import org.apache.tapestry5.services.javascript.JavaScriptSupport;
032import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
033import org.apache.tapestry5.services.transform.TransformationSupport;
034
035import java.util.ArrayList;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Set;
039
040/**
041 * Implements the {@link Import} annotation, both at the class and at the method level.
042 *
043 * @since 5.2.0
044 */
045public class ImportWorker implements ComponentClassTransformWorker2
046{
047    
048    private static final String FIELD_PREFIX = "importedAssets_";
049    
050    private final JavaScriptSupport javascriptSupport;
051
052    private final SymbolSource symbolSource;
053
054    private final AssetSource assetSource;
055    
056    private final ResourceChangeTracker resourceChangeTracker;
057    
058    private final boolean multipleClassLoaders;
059    
060    private final PropertyValueProviderWorker propertyValueProviderWorker;
061    
062    private static enum ImportType 
063    {
064        LIBRARY, STYLESHEET
065    }
066
067    private final Worker<Asset> importLibrary = new Worker<Asset>()
068    {
069        public void work(Asset asset)
070        {
071            javascriptSupport.importJavaScriptLibrary(asset);
072        }
073    };
074
075    private final Worker<Asset> importStylesheet = new Worker<Asset>()
076    {
077        public void work(Asset asset)
078        {
079            javascriptSupport.importStylesheet(asset);
080        }
081    };
082
083    private final Mapper<String, String> expandSymbols = new Mapper<String, String>()
084    {
085        public String map(String element)
086        {
087            return symbolSource.expandSymbols(element);
088        }
089    };
090
091    public ImportWorker(JavaScriptSupport javascriptSupport, SymbolSource symbolSource, AssetSource assetSource,
092            ResourceChangeTracker resourceChangeTracker, PropertyValueProviderWorker propertyValueProviderWorker)
093    {
094        this.javascriptSupport = javascriptSupport;
095        this.symbolSource = symbolSource;
096        this.assetSource = assetSource;
097        this.resourceChangeTracker = resourceChangeTracker;
098        this.propertyValueProviderWorker = propertyValueProviderWorker;
099        this.multipleClassLoaders = 
100                !Boolean.valueOf(symbolSource.valueForSymbol(SymbolConstants.PRODUCTION_MODE)) &&
101                Boolean.valueOf(symbolSource.valueForSymbol(SymbolConstants.MULTIPLE_CLASSLOADERS));
102    }
103
104    public void transform(PlasticClass componentClass, TransformationSupport support, MutableComponentModel model)
105    {
106        resourceChangeTracker.setCurrentClassName(model.getComponentClassName());
107        
108        Set<PlasticUtils.FieldInfo> fieldInfos = multipleClassLoaders ? new HashSet<>() : null;
109        processClassAnnotationAtSetupRenderPhase(componentClass, model, fieldInfos);
110
111        final List<PlasticMethod> methods = componentClass.getMethodsWithAnnotation(Import.class);
112        for (PlasticMethod m : methods)
113        {
114            decorateMethod(componentClass, model, m, fieldInfos);
115        }
116        
117        if (multipleClassLoaders && !fieldInfos.isEmpty())
118        {
119            propertyValueProviderWorker.add(componentClass, fieldInfos);
120        }
121        
122        resourceChangeTracker.clearCurrentClassName();
123    }
124
125    private void processClassAnnotationAtSetupRenderPhase(PlasticClass componentClass, MutableComponentModel model,
126            Set<FieldInfo> fieldInfos)
127    {
128        Import annotation = componentClass.getAnnotation(Import.class);
129
130        if (annotation != null)
131        {
132            PlasticMethod setupRender = componentClass.introduceMethod(TransformConstants.SETUP_RENDER_DESCRIPTION);
133
134            decorateMethod(componentClass, model, setupRender, annotation, fieldInfos);
135
136            model.addRenderPhase(SetupRender.class);
137        }
138    }
139
140    private void decorateMethod(PlasticClass componentClass, MutableComponentModel model, PlasticMethod method, Set<FieldInfo> fieldInfos)
141    {
142        Import annotation = method.getAnnotation(Import.class);
143
144        decorateMethod(componentClass, model, method, annotation, fieldInfos);
145    }
146
147    private void decorateMethod(PlasticClass componentClass, MutableComponentModel model, PlasticMethod method,
148                                Import annotation, Set<FieldInfo> fieldInfos)
149    {
150        importStacks(method, annotation.stack());
151
152        importLibraries(componentClass, model, method, annotation.library(), fieldInfos);
153
154        importStylesheets(componentClass, model, method, annotation.stylesheet(), fieldInfos);
155
156        importModules(method, annotation.module());
157    }
158
159    private void importStacks(PlasticMethod method, String[] stacks)
160    {
161        if (stacks.length != 0)
162        {
163            method.addAdvice(createImportStackAdvice(stacks));
164        }
165    }
166
167    private MethodAdvice createImportStackAdvice(final String[] stacks)
168    {
169        return new MethodAdvice()
170        {
171            public void advise(MethodInvocation invocation)
172            {
173                for (String stack : stacks)
174                {
175                    javascriptSupport.importStack(stack);
176                }
177
178                invocation.proceed();
179            }
180        };
181    }
182
183    private void importModules(PlasticMethod method, String[] moduleNames)
184    {
185        if (moduleNames.length != 0)
186        {
187            method.addAdvice(createImportModulesAdvice(moduleNames));
188        }
189    }
190
191    class ModuleImport
192    {
193        final String moduleName, functionName;
194
195        ModuleImport(String moduleName, String functionName)
196        {
197            this.moduleName = moduleName;
198            this.functionName = functionName;
199        }
200
201        void apply(JavaScriptSupport javaScriptSupport)
202        {
203            Initialization initialization = javaScriptSupport.require(moduleName);
204
205            if (functionName != null)
206            {
207                initialization.invoke(functionName);
208            }
209        }
210    }
211
212    private MethodAdvice createImportModulesAdvice(final String[] moduleNames)
213    {
214        final List<ModuleImport> moduleImports = new ArrayList<ModuleImport>(moduleNames.length);
215
216        for (String name : moduleNames)
217        {
218            int colonx = name.indexOf(':');
219
220            String moduleName = colonx < 0 ? name : name.substring(0, colonx);
221            String functionName = colonx < 0 ? null : name.substring(colonx + 1);
222
223            moduleImports.add(new ModuleImport(moduleName, functionName));
224        }
225
226        return new MethodAdvice()
227        {
228            public void advise(MethodInvocation invocation)
229            {
230                for (ModuleImport moduleImport : moduleImports)
231                {
232                    moduleImport.apply(javascriptSupport);
233                }
234
235                invocation.proceed();
236            }
237        };
238    }
239
240    private void importLibraries(PlasticClass plasticClass, MutableComponentModel model, PlasticMethod method,
241                                 String[] paths, Set<FieldInfo> fieldInfos)
242    {
243        decorateMethodWithOperation(plasticClass, model, method, paths, importLibrary, fieldInfos, ImportType.LIBRARY);
244    }
245
246    private void importStylesheets(PlasticClass plasticClass, MutableComponentModel model, PlasticMethod method,
247                                   String[] paths, Set<FieldInfo> fieldInfos)
248    {
249        decorateMethodWithOperation(plasticClass, model, method, paths, importStylesheet, fieldInfos, ImportType.STYLESHEET);
250    }
251
252    private void decorateMethodWithOperation(PlasticClass componentClass, MutableComponentModel model,
253                                             PlasticMethod method, String[] paths, Worker<Asset> operation,
254                                             Set<FieldInfo> fieldInfos, ImportType importType)
255    {
256        if (paths.length == 0)
257        {
258            return;
259        }
260
261        String[] expandedPaths = expandPaths(paths);
262
263        final String fieldName = getFieldName(method, importType);
264        PlasticField assetListField = componentClass.introduceField(Asset[].class, fieldName);
265        
266        if (multipleClassLoaders)
267        {
268            fieldInfos.add(PlasticUtils.toFieldInfo(assetListField));
269            assetListField.createAccessors(PropertyAccessType.READ_ONLY);
270        }
271
272        initializeAssetsFromPaths(expandedPaths, assetListField, model.getLibraryName());
273
274        addMethodAssetOperationAdvice(method, assetListField.getHandle(), operation, importType);
275    }
276
277    private String getFieldName(PlasticMethod method, ImportType importType) 
278    {
279        final StringBuilder builder = new StringBuilder(FIELD_PREFIX);
280        builder.append(method.getDescription().methodName);
281        if (multipleClassLoaders)
282        {
283            builder.append("_");
284            builder.append(method.getPlasticClass().getClassName().replace('.', '_'));
285            builder.append("_");
286            builder.append(importType.name().toLowerCase());
287        }
288        return builder.toString();
289    }
290
291    private String[] expandPaths(String[] paths)
292    {
293        return F.flow(paths).map(expandSymbols).toArray(String.class);
294    }
295
296    private void initializeAssetsFromPaths(final String[] expandedPaths, PlasticField assetsField, final String libraryName)
297    {
298        assetsField.injectComputed(new ComputedValue<Asset[]>()
299        {
300            public Asset[] get(InstanceContext context)
301            {
302                ComponentResources resources = context.get(ComponentResources.class);
303
304                return convertPathsToAssetArray(resources, expandedPaths, libraryName);
305            }
306        });
307    }
308
309    private Asset[] convertPathsToAssetArray(final ComponentResources resources, String[] assetPaths, final String libraryName)
310    {
311        return F.flow(assetPaths).map(new Mapper<String, Asset>()
312        {
313            public Asset map(String assetPath)
314            {
315                return assetSource.getComponentAsset(resources, assetPath, libraryName);
316            }
317        }).toArray(Asset.class);
318    }
319
320    private void addMethodAssetOperationAdvice(PlasticMethod method, final FieldHandle access,
321                                               final Worker<Asset> operation, ImportType importType)
322    {
323        final String className = method.getPlasticClass().getClassName();
324        final String fieldName = getFieldName(method, importType);
325        method.addAdvice(new MethodAdvice()
326        {
327            public void advise(MethodInvocation invocation)
328            {
329                invocation.proceed();
330
331                final Object instance = invocation.getInstance();
332                Asset[] assets = (Asset[]) (multipleClassLoaders ?
333                        PropertyValueProvider.get(instance, fieldName) :
334                        access.get(instance));
335
336                if (multipleClassLoaders)
337                {
338                    resourceChangeTracker.setCurrentClassName(className);
339                }
340                
341                F.flow(assets).each(operation);
342                
343                if (multipleClassLoaders)
344                {
345                    resourceChangeTracker.clearCurrentClassName();
346                }
347            }
348        });
349    }
350    
351}