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