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.services.javascript; 016 017import java.io.ByteArrayInputStream; 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.SequenceInputStream; 021import java.net.URL; 022import java.util.LinkedHashMap; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.Vector; 026 027import org.apache.tapestry5.func.F; 028import org.apache.tapestry5.func.Flow; 029import org.apache.tapestry5.func.Mapper; 030import org.apache.tapestry5.func.Predicate; 031import org.apache.tapestry5.internal.util.VirtualResource; 032import org.apache.tapestry5.ioc.Resource; 033import org.apache.tapestry5.ioc.internal.util.InternalUtils; 034 035/** 036 * Used to wrap plain JavaScript libraries as AMD modules. The underlying 037 * resource is transformed before it is sent to the client. 038 * <p> 039 * This is an alternative to configuring RequireJS module shims for the 040 * libraries. As opposed to shimmed libraries, the modules created using the 041 * AMDWrapper can be added to JavaScript stacks. 042 * <p> 043 * If the library depends on global variables, these can be added as module 044 * dependencies. For a library that expects jQuery to be available as 045 * <code>$<code>, the wrapper should be setup calling <code>require("jQuery", "$")<code> 046 * on the respective wrapper. 047 * 048 * @since 5.4 049 * @see JavaScriptModuleConfiguration 050 * @see ModuleManager 051 */ 052public class AMDWrapper { 053 054 /** 055 * The underlying resource, usually a JavaScript library 056 */ 057 private final Resource resource; 058 059 /** 060 * The modules that this module requires, the keys being module names and 061 * the values being the respective parameter names for the module's factory 062 * function. 063 */ 064 private final Map<String, String> requireConfig = new LinkedHashMap<String, String>(); 065 066 /** 067 * The expression that determines what is returned from the factory function 068 */ 069 private String returnExpression; 070 071 public AMDWrapper(final Resource resource) { 072 this.resource = resource; 073 } 074 075 /** 076 * Add a dependency on another module. The module will be passed into the 077 * generated factory function as a parameter. 078 * 079 * @param moduleName 080 * the name of the required module, e.g. <code>jQuery</code> 081 * @param parameterName 082 * the module's corresponding parameter name of the factory 083 * function, e.g. <code>$</code> 084 * @return this AMDWrapper for further configuration 085 */ 086 public AMDWrapper require(final String moduleName, 087 final String parameterName) { 088 requireConfig.put(moduleName, parameterName); 089 return this; 090 } 091 092 /** 093 * Add a dependency on another module. The module will be loaded but not 094 * passed to the factory function. This is useful for dependencies on other 095 * modules that do not actually return a value. 096 * 097 * @param moduleName 098 * the name of the required module, e.g. 099 * <code>bootstrap/transition</code> 100 * @return this AMDWrapper for further configuration 101 */ 102 public AMDWrapper require(final String moduleName) { 103 requireConfig.put(moduleName, null); 104 return this; 105 } 106 107 /** 108 * Optionally sets a return expression for this module. If the underlying 109 * library creates a global variable, this is usually what is returned here. 110 * 111 * @param returnExpression 112 * the expression that is returned from this module (e.g. 113 * <code>Raphael</code>) 114 * @return this AMDWrapper for further configuration 115 */ 116 public AMDWrapper setReturnExpression(final String returnExpression) { 117 this.returnExpression = returnExpression; 118 return this; 119 } 120 121 /** 122 * Return this wrapper instance as a {@link JavaScriptModuleConfiguration}, 123 * so it can be contributed to the {@link ModuleManager}'s configuration. 124 * The resulting {@link JavaScriptModuleConfiguration} should not be 125 * changed. 126 * 127 * @return a {@link JavaScriptModuleConfiguration} for this AMD wrapper 128 */ 129 public JavaScriptModuleConfiguration asJavaScriptModuleConfiguration() { 130 return new JavaScriptModuleConfiguration(transformResource()); 131 } 132 133 private Resource transformResource() { 134 return new AMDModuleWrapperResource(resource, requireConfig, 135 returnExpression); 136 } 137 138 /** 139 * A virtual resource that wraps a plain JavaScript library as an AMD 140 * module. 141 * 142 */ 143 private final static class AMDModuleWrapperResource extends VirtualResource { 144 private final Resource resource; 145 private final Map<String, String> requireConfig; 146 private final String returnExpression; 147 148 public AMDModuleWrapperResource(final Resource resource, 149 final Map<String, String> requireConfig, 150 final String returnExpression) { 151 this.resource = resource; 152 this.requireConfig = requireConfig; 153 this.returnExpression = returnExpression; 154 155 } 156 157 @Override 158 public InputStream openStream() throws IOException { 159 InputStream leaderStream; 160 InputStream trailerStream; 161 162 StringBuilder sb = new StringBuilder(); 163 164 // create a Flow of map entries (module name to factory function 165 // parameter name) 166 Flow<Entry<String, String>> requiredModulesToNames = F 167 .flow(requireConfig.entrySet()); 168 169 // some of the modules are not passed to the factory, sort them last 170 Flow<Entry<String, String>> requiredModulesToNamesNamedFirst = requiredModulesToNames 171 .remove(VALUE_IS_NULL).concat( 172 requiredModulesToNames.filter(VALUE_IS_NULL)); 173 174 sb.append("define(["); 175 sb.append(InternalUtils.join(requiredModulesToNamesNamedFirst 176 .map(GET_KEY).map(QUOTE).toList())); 177 sb.append("], function("); 178 179 // append only the modules that should be passed to the factory 180 // function, i.e. those whose map entry value is not null 181 sb.append(InternalUtils.join(F.flow(requireConfig.values()) 182 .filter(F.notNull()).toList())); 183 sb.append("){\n"); 184 leaderStream = toInputStream(sb); 185 sb.setLength(0); 186 187 if (returnExpression != null) 188 { 189 sb.append("\nreturn "); 190 sb.append(returnExpression); 191 sb.append(";"); 192 } 193 sb.append("\n});"); 194 trailerStream = toInputStream(sb); 195 196 Vector<InputStream> v = new Vector<InputStream>(3); 197 v.add(leaderStream); 198 v.add(resource.openStream()); 199 v.add(trailerStream); 200 201 return new SequenceInputStream(v.elements()); 202 } 203 204 @Override 205 public String getFile() { 206 return "generated-module-for-" + resource.getFile(); 207 } 208 209 @Override 210 public URL toURL() { 211 return null; 212 } 213 214 @Override 215 public String toString() { 216 return "AMD module wrapper for " + resource.toString(); 217 } 218 219 private static InputStream toInputStream(final StringBuilder sb) { 220 return new ByteArrayInputStream(sb.toString().getBytes(UTF8)); 221 222 } 223 } 224 225 private final static Mapper<Entry<String, String>, String> GET_KEY = new Mapper<Entry<String, String>, String>() { 226 227 @Override 228 public String map(final Entry<String, String> element) { 229 return element.getKey(); 230 } 231 232 }; 233 234 private final static Predicate<Entry<String, String>> VALUE_IS_NULL = new Predicate<Entry<String, String>>() { 235 236 @Override 237 public boolean accept(final Entry<String, String> element) { 238 return element.getValue() == null; 239 } 240 241 }; 242 243 private final static Mapper<String, String> QUOTE = new Mapper<String, String>() { 244 245 @Override 246 public String map(final String element) { 247 StringBuilder sb = new StringBuilder(element.length() + 2); 248 sb.append('"'); 249 sb.append(element); 250 sb.append('"'); 251 return sb.toString(); 252 } 253 }; 254 255}