    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.services.ajax;
    014
    015import org.apache.tapestry5.Asset;
    016import org.apache.tapestry5.BooleanHook;
    017import org.apache.tapestry5.ComponentResources;
    018import org.apache.tapestry5.FieldFocusPriority;
    019import org.apache.tapestry5.commons.util.CollectionFactory;
    020import org.apache.tapestry5.func.F;
    021import org.apache.tapestry5.func.Worker;
    022import org.apache.tapestry5.internal.InternalConstants;
    023import org.apache.tapestry5.internal.services.DocumentLinker;
    024import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor;
    025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026import org.apache.tapestry5.ioc.util.IdAllocator;
    027import org.apache.tapestry5.json.JSONArray;
    028import org.apache.tapestry5.json.JSONObject;
    029import org.apache.tapestry5.services.javascript.*;
    030
    031import java.util.*;
    032
    033public class JavaScriptSupportImpl implements JavaScriptSupport
    034{
    035    private final IdAllocator idAllocator;
    036
    037    private final DocumentLinker linker;
    038
    039    // Using a Map as a case-insensitive set of stack names.
    040
    041    private final Map<String, Boolean> addedStacks = CollectionFactory.newCaseInsensitiveMap();
    042
    043    private final Set<String> otherLibraries = CollectionFactory.newSet();
    044
    045    private final Set<String> importedStylesheetURLs = CollectionFactory.newSet();
    046
    047    private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList();
    048
    049    private final List<InitializationImpl> inits = CollectionFactory.newList();
    050
    051    private final JavaScriptStackSource javascriptStackSource;
    052
    053    private final JavaScriptStackPathConstructor stackPathConstructor;
    054
    055    private final boolean partialMode;
    056
    057    private final BooleanHook suppressCoreStylesheetsHook;
    058
    059    private FieldFocusPriority focusPriority;
    060
    061    private String focusFieldId;
    062
    063    private Map<String, String> libraryURLToStackName, moduleNameToStackName;
    064
    065    class InitializationImpl implements Initialization
    066    {
    067        InitializationPriority priority = InitializationPriority.NORMAL;
    068
    069        final String moduleName;
    070
    071        String functionName;
    072
    073        JSONArray arguments;
    074
    075        InitializationImpl(String moduleName)
    076        {
    077            this.moduleName = moduleName;
    078        }
    079
    080        public Initialization invoke(String functionName)
    081        {
    082            assert InternalUtils.isNonBlank(functionName);
    083
    084            this.functionName = functionName;
    085
    086            return this;
    087        }
    088
    089        public Initialization priority(InitializationPriority priority)
    090        {
    091            assert priority != null;
    092
    093            this.priority = priority;
    094
    095            return this;
    096        }
    097
    098        public void with(Object... arguments)
    099        {
    100            assert arguments != null;
    101
    102            this.arguments = new JSONArray(arguments);
    103        }
    104    }
    105
    106    public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
    107                                 JavaScriptStackPathConstructor stackPathConstructor, BooleanHook suppressCoreStylesheetsHook)
    108    {
    109        this(linker, javascriptStackSource, stackPathConstructor, new IdAllocator(), false, suppressCoreStylesheetsHook);
    110    }
    111
    112    /**
    113     * @param linker
    114     *         responsible for assembling all the information gathered by JavaScriptSupport and
    115     *         attaching it to the Document (for a full page render) or to the JSON response (in a partial render)
    116     * @param javascriptStackSource
    117     *         source of information about {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s, used when handling the import
    118     *         of libraries and stacks (often, to handle transitive dependencies)
    119     * @param stackPathConstructor
    120     *         encapsulates the knowledge of how to represent a stack (which may be converted
    121     *         from a series of JavaScript libraries into a single virtual JavaScript library)
    122     * @param idAllocator
    123     *         used when allocating unique ids (it is usually pre-initialized in an Ajax request to ensure
    124     *         that newly allocated ids do not conflict with previous renders and partial updates)
    125     * @param partialMode
    126     *         if true, then the JSS configures itself for a partial page render (part of an Ajax request)
    127     *         which automatically assumes the "core" library has been added (to the original page render)
    128     * @param suppressCoreStylesheetsHook
    129     *         a hook that enables ignoring CSS files on the core stack
    130     */
    131    public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
    132                                 JavaScriptStackPathConstructor stackPathConstructor, IdAllocator idAllocator, boolean partialMode,
    133                                 BooleanHook suppressCoreStylesheetsHook)
    134    {
    135        this.linker = linker;
    136        this.idAllocator = idAllocator;
    137        this.javascriptStackSource = javascriptStackSource;
    138        this.stackPathConstructor = stackPathConstructor;
    139        this.partialMode = partialMode;
    140        this.suppressCoreStylesheetsHook = suppressCoreStylesheetsHook;
    141
    142        // In partial mode, assume that the infrastructure stack is already present
    143        // (from the original page render).
    144
    145        if (partialMode)
    146        {
    147            addedStacks.put(InternalConstants.CORE_STACK_NAME, true);
    148        }
    149    }
    150
    151    public void commit()
    152    {
    153        if (focusFieldId != null)
    154        {
    155            require("t5/core/pageinit").invoke("focus").with(focusFieldId);
    156        }
    157
    158        F.flow(stylesheetLinks).each(new Worker<StylesheetLink>()
    159        {
    160            public void work(StylesheetLink value)
    161            {
    162                linker.addStylesheetLink(value);
    163            }
    164        });
    165
    166        F.flow(inits).sort(new Comparator<InitializationImpl>()
    167        {
    168            public int compare(InitializationImpl o1, InitializationImpl o2)
    169            {
    170                return o1.priority.compareTo(o2.priority);
    171            }
    172        }).each(new Worker<InitializationImpl>()
    173        {
    174            public void work(InitializationImpl element)
    175            {
    176                linker.addInitialization(element.priority, element.moduleName, element.functionName, element.arguments);
    177            }
    178        });
    179    }
    180
    181    public void addInitializerCall(InitializationPriority priority, String functionName, JSONObject parameter)
    182    {
    183        createInitializer(priority).with(functionName, parameter);
    184    }
    185
    186    public void addInitializerCall(String functionName, JSONArray parameter)
    187    {
    188        addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
    189    }
    190
    191    public void addInitializerCall(InitializationPriority priority, String functionName,
    192                                   JSONArray parameter)
    193    {
    194        // TAP5-2300: In 5.3, a JSONArray implied an array of method arguments, so unwrap and add
    195        // functionName to the arguments
    196
    197        List parameterList = new ArrayList(parameter.length() + 1);
    198        parameterList.add(functionName);
    199        parameterList.addAll(parameter.toList());
    200        createInitializer(priority).with(parameterList.toArray());
    201    }
    202
    203    private Initialization createInitializer(InitializationPriority priority)
    204    {
    205        assert priority != null;
    206
    207        importCoreStack();
    208
    209        return require("t5/core/init").priority(priority);
    210    }
    211
    212    public void addInitializerCall(String functionName, JSONObject parameter)
    213    {
    214        addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
    215    }
    216
    217    public void addInitializerCall(InitializationPriority priority, String functionName, String parameter)
    218    {
    219        createInitializer(priority).with(functionName, parameter);
    220    }
    221
    222    public void addInitializerCall(String functionName, String parameter)
    223    {
    224        addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
    225    }
    226
    227    public void addScript(InitializationPriority priority, String format, Object... arguments)
    228    {
    229        assert priority != null;
    230        assert InternalUtils.isNonBlank(format);
    231
    232        importCoreStack();
    233
    234        String newScript = arguments.length == 0 ? format : String.format(format, arguments);
    235
    236        if (partialMode)
    237        {
    238            require("t5/core/pageinit").invoke("evalJavaScript").with(newScript);
    239        } else
    240        {
    241            linker.addScript(priority, newScript);
    242        }
    243    }
    244
    245    public void addScript(String format, Object... arguments)
    246    {
    247        addScript(InitializationPriority.NORMAL, format, arguments);
    248    }
    249
    250    public void addModuleConfigurationCallback(ModuleConfigurationCallback callback)
    251    {
    252        linker.addModuleConfigurationCallback(callback);
    253    }
    254
    255    public String allocateClientId(ComponentResources resources)
    256    {
    257        return allocateClientId(resources.getId());
    258    }
    259
    260    public String allocateClientId(String id)
    261    {
    262        return idAllocator.allocateId(id);
    263    }
    264
    265    public JavaScriptSupport importJavaScriptLibrary(Asset asset)
    266    {
    267        assert asset != null;
    268
    269        return importJavaScriptLibrary(asset.toClientURL());
    270    }
    271
    272    public JavaScriptSupport importJavaScriptLibrary(String libraryURL)
    273    {
    274        importCoreStack();
    275
    276        String stackName = findStackForLibrary(libraryURL);
    277
    278        if (stackName != null)
    279        {
    280            return importStack(stackName);
    281        }
    282
    283        if (!otherLibraries.contains(libraryURL))
    284        {
    285            linker.addLibrary(libraryURL);
    286
    287            otherLibraries.add(libraryURL);
    288        }
    289
    290        return this;
    291    }
    292
    293    private void importCoreStack()
    294    {
    295        addAssetsFromStack(InternalConstants.CORE_STACK_NAME);
    296    }
    297
    298    /**
    299     * Locates the name of the stack that includes the library URL. Returns the stack,
    300     * or null if the library is free-standing.
    301     */
    302    private String findStackForLibrary(String libraryURL)
    303    {
    304        return getLibraryURLToStackName().get(libraryURL);
    305    }
    306
    307
    308    private Map<String, String> getLibraryURLToStackName()
    309    {
    310        if (libraryURLToStackName == null)
    311        {
    312            libraryURLToStackName = CollectionFactory.newMap();
    313
    314            for (String stackName : javascriptStackSource.getStackNames())
    315            {
    316                for (Asset library : javascriptStackSource.getStack(stackName).getJavaScriptLibraries())
    317                {
    318                    libraryURLToStackName.put(library.toClientURL(), stackName);
    319                }
    320            }
    321        }
    322
    323        return libraryURLToStackName;
    324    }
    325
    326    private String findStackForModule(String moduleName)
    327    {
    328        return getModuleNameToStackName().get(moduleName);
    329    }
    330
    331    private Map<String, String> getModuleNameToStackName()
    332    {
    333
    334        if (moduleNameToStackName == null)
    335        {
    336            moduleNameToStackName = CollectionFactory.newMap();
    337
    338            for (String stackName : javascriptStackSource.getStackNames())
    339            {
    340                for (String moduleName : javascriptStackSource.getStack(stackName).getModules())
    341                {
    342                    moduleNameToStackName.put(moduleName, stackName);
    343                }
    344            }
    345        }
    346
    347        return moduleNameToStackName;
    348    }
    349
    350
    351    private void addAssetsFromStack(String stackName)
    352    {
    353        if (addedStacks.containsKey(stackName))
    354        {
    355            return;
    356        }
    357
    358        JavaScriptStack stack = javascriptStackSource.getStack(stackName);
    359
    360        for (String dependentStackname : stack.getStacks())
    361        {
    362            addAssetsFromStack(dependentStackname);
    363        }
    364
    365        addedStacks.put(stackName, true);
    366
    367        boolean addAsCoreLibrary = stackName.equals(InternalConstants.CORE_STACK_NAME);
    368
    369        List<String> libraryURLs = stackPathConstructor.constructPathsForJavaScriptStack(stackName);
    370
    371        for (String libraryURL : libraryURLs)
    372        {
    373            if (addAsCoreLibrary)
    374            {
    375                linker.addCoreLibrary(libraryURL);
    376            } else
    377            {
    378                linker.addLibrary(libraryURL);
    379            }
    380        }
    381
    382        if (!(addAsCoreLibrary && suppressCoreStylesheetsHook.checkHook()))
    383        {
    384            stylesheetLinks.addAll(stack.getStylesheets());
    385        }
    386
    387        String initialization = stack.getInitialization();
    388
    389        if (initialization != null)
    390        {
    391            addScript(InitializationPriority.IMMEDIATE, initialization);
    392        }
    393    }
    394
    395    public JavaScriptSupport importStylesheet(Asset stylesheet)
    396    {
    397        assert stylesheet != null;
    398
    399        return importStylesheet(new StylesheetLink(stylesheet));
    400    }
    401
    402    public JavaScriptSupport importStylesheet(StylesheetLink stylesheetLink)
    403    {
    404        assert stylesheetLink != null;
    405
    406        importCoreStack();
    407
    408        String stylesheetURL = stylesheetLink.getURL();
    409
    410        if (!importedStylesheetURLs.contains(stylesheetURL))
    411        {
    412            importedStylesheetURLs.add(stylesheetURL);
    413
    414            stylesheetLinks.add(stylesheetLink);
    415        }
    416
    417        return this;
    418    }
    419
    420    public JavaScriptSupport importStack(String stackName)
    421    {
    422        assert InternalUtils.isNonBlank(stackName);
    423
    424        importCoreStack();
    425
    426        addAssetsFromStack(stackName);
    427
    428        return this;
    429    }
    430
    431    public JavaScriptSupport autofocus(FieldFocusPriority priority, String fieldId)
    432    {
    433        assert priority != null;
    434        assert InternalUtils.isNonBlank(fieldId);
    435
    436        if (focusFieldId == null || priority.compareTo(focusPriority) > 0)
    437        {
    438            this.focusPriority = priority;
    439            focusFieldId = fieldId;
    440        }
    441
    442        return this;
    443    }
    444
    445    public Initialization require(String moduleName)
    446    {
    447        assert InternalUtils.isNonBlank(moduleName);
    448
    449        importCoreStack();
    450
    451        String stackName = findStackForModule(moduleName);
    452
    453        if (stackName != null)
    454        {
    455            importStack(stackName);
    456        }
    457
    458        InitializationImpl init = new InitializationImpl(moduleName);
    459
    460        inits.add(init);
    461
    462        return init;
    463    }
    464
    465}