001 /* 002 * 003 * This file is part of DirectJNgine. 004 * 005 * DirectJNgine is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License. 008 * 009 * DirectJNgine is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with DirectJNgine. If not, see <http://www.gnu.org/licenses/>. 016 * 017 * This software uses the ExtJs library (http://extjs.com), which is 018 * distributed under the GPL v3 license (see http://extjs.com/license). 019 */ 020 021 package com.softwarementors.extjs.djn.config; 022 023 import java.io.File; 024 import java.io.IOException; 025 import java.lang.reflect.Method; 026 import java.util.ArrayList; 027 import java.util.HashMap; 028 import java.util.List; 029 import java.util.Map; 030 031 import org.apache.commons.io.FileUtils; 032 import org.apache.log4j.Logger; 033 034 import com.softwarementors.extjs.djn.ClassUtils; 035 import com.softwarementors.extjs.djn.StringUtils; 036 import com.softwarementors.extjs.djn.api.ApiCodeGenerator; 037 import com.softwarementors.extjs.djn.config.annotations.DirectAction; 038 import com.softwarementors.extjs.djn.config.annotations.DirectFormPostMethod; 039 import com.softwarementors.extjs.djn.config.annotations.DirectMethod; 040 import com.softwarementors.extjs.djn.config.annotations.DirectPollMethod; 041 042 public class Registry { 043 044 private static final Logger logger = Logger.getLogger( Registry.class); 045 046 private Map<String, RegisteredAction> actionsByName = new HashMap<String,RegisteredAction>(); 047 private Map<Class<?>, RegisteredAction> actionsByClass = new HashMap<Class<?>, RegisteredAction>(); 048 private Map<String, RegisteredPollMethod> pollMethods = new HashMap<String, RegisteredPollMethod>(); 049 private List<RegisteredAction> currentApiActions; 050 private List<RegisteredPollMethod> currentApiPollMethods; 051 private Map<String, RegisteredApi> apis = new HashMap<String, RegisteredApi>(); 052 053 private boolean isActionClassRegistered( Class<?> actionClass ) { 054 assert actionClass != null; 055 056 return this.actionsByClass.containsKey(actionClass); 057 } 058 059 private RegisteredAction registerActionClass( Class<?> actionClass ) { 060 assert actionClass != null; 061 062 if( isActionClassRegistered( actionClass ) ) { 063 ApiConfigurationException ex = ApiConfigurationException.forClassAlreadyRegisteredAsAction(actionClass); 064 logger.fatal( ex.getMessage(), ex ); 065 throw ex; 066 } 067 068 if( logger.isDebugEnabled() ) { 069 logger.debug( "Scanning Java class: " + actionClass.getName() ); 070 } 071 072 RegisteredAction action = createAction(actionClass ); 073 String actionName = action.getName(); 074 075 if( isActionRegistered( actionName ) ) { 076 RegisteredAction existingAction = this.actionsByName.get( actionName ); 077 ApiConfigurationException ex = ApiConfigurationException.forActionAlreadyRegistered(action, existingAction.getActionClass()); 078 logger.fatal( ex.getMessage(), ex ); 079 throw ex; 080 } 081 082 this.actionsByClass.put( actionClass, action); 083 this.actionsByName.put( actionName, action ); 084 this.currentApiActions.add( action ); 085 086 if( logger.isDebugEnabled() ) { 087 logger.debug( "Finished scanning Java class: " + actionClass.getName() ); 088 } 089 090 return action; 091 } 092 093 private RegisteredAction createAction(Class<?> actionClass) { 094 assert actionClass != null; 095 096 RegisteredAction action = createActionFromAnnotation(actionClass); 097 Map<String, RegisteredMethod> methodsByName = createMethods(action); 098 action.setMethods( methodsByName ); 099 100 return action; 101 } 102 103 private RegisteredAction createActionFromAnnotation(Class<?> actionClass) { 104 assert actionClass != null; 105 106 DirectAction actionAnnotation = actionClass.getAnnotation(DirectAction.class); 107 String actionName = ""; 108 if( actionAnnotation != null ) { 109 actionName = actionAnnotation.action(); 110 } 111 112 if( actionName.equals("")) { 113 actionName = ClassUtils.getSimpleName(actionClass); 114 } 115 return new RegisteredAction( actionClass, actionName ); 116 } 117 118 private static final String POLL_METHOD_NAME_PREFIX = "djnpoll_"; 119 private static final String FORM_POST_METHOD_NAME_PREFIX = "djnform_"; 120 private static final String STANDARD_METHOD_NAME_PREFIX = "djn_"; 121 122 private Map<String, RegisteredMethod> createMethods(RegisteredAction action) { 123 assert action != null; 124 125 // *All* methods are candidates, including those in base classes, 126 // even if the base class does not have a DirectAction annotation! 127 Method[] methods = action.getActionClass().getDeclaredMethods(); // Get private, protected and other methods! 128 129 Map<String,RegisteredMethod> methodsByName = new HashMap<String,RegisteredMethod>(); 130 for( Method method : methods ) { 131 // Check if the kind of direct method -if any 132 DirectMethod methodAnnotation = method.getAnnotation(DirectMethod.class); 133 boolean isStandardMethod = methodAnnotation != null; 134 if( !isStandardMethod ) { 135 isStandardMethod = method.getName().startsWith(STANDARD_METHOD_NAME_PREFIX); 136 } 137 138 DirectFormPostMethod postMethodAnnotation = method.getAnnotation(DirectFormPostMethod.class); 139 boolean isFormPostMethod = postMethodAnnotation != null; 140 if( !isFormPostMethod ) { 141 isFormPostMethod = method.getName().startsWith(FORM_POST_METHOD_NAME_PREFIX); 142 } 143 144 DirectPollMethod pollMethodAnnotation = method.getAnnotation(DirectPollMethod.class); 145 boolean isPollMethod = pollMethodAnnotation != null; 146 if( !isPollMethod ) { 147 isPollMethod = method.getName().startsWith( POLL_METHOD_NAME_PREFIX ); 148 } 149 150 // Check that a method is just of only one kind of method 151 if( isStandardMethod && isFormPostMethod ) { 152 ApiConfigurationException ex = ApiConfigurationException.forMethodCantBeStandardAndFormPostMethodAtTheSameTime(action, method); 153 logger.fatal( ex.getMessage(), ex ); 154 throw ex; 155 } 156 if( (methodAnnotation != null || postMethodAnnotation != null) && isPollMethod) { 157 ApiConfigurationException ex = ApiConfigurationException.forPollMethodCantBeStandardOrFormPostMethodAtTheSameTime(action, method); 158 logger.fatal( ex.getMessage(), ex ); 159 throw ex; 160 } 161 162 // Process standard and form post methods together, as they are very similar 163 if( isStandardMethod || isFormPostMethod) { 164 String methodName = ""; 165 if( isStandardMethod ) { 166 methodName = getStandardMethodName(method, methodAnnotation); 167 } 168 else { 169 methodName = getFormPostMethodName( method, postMethodAnnotation); 170 } 171 RegisteredMethod actionMethod = new RegisteredMethod( action, methodName, method, isFormPostMethod ); 172 173 if( methodsByName.containsKey(methodName) ) { 174 ApiConfigurationException ex = ApiConfigurationException.forMethodAlreadyRegisteredInAction(actionMethod); 175 logger.fatal( ex.getMessage(), ex ); 176 throw ex; 177 } 178 179 methodsByName.put( methodName, actionMethod); 180 181 if( actionMethod.getFormHandler() ) { 182 if( logger.isDebugEnabled() ) { 183 logger.debug( " - Registered new Form Method. Name: '" + actionMethod.getFullName() + "'. Java method: '" + action.getActionClass().getName() + "." + method.getName() + "'" ); 184 } 185 } 186 else { 187 if( logger.isDebugEnabled() ) { 188 logger.debug( " - Registered new Standard Method. Name: '" + actionMethod.getFullName() + "'. Java method: '" + action.getActionClass().getName() + "." + method.getName() + "'" ); 189 } 190 } 191 } 192 193 // Process "poll" method 194 if( isPollMethod ) { 195 processPollMethod(action, method, pollMethodAnnotation); 196 } 197 } 198 199 return methodsByName; 200 } 201 202 private String getFormPostMethodName(Method method, DirectFormPostMethod postMethodAnnotation) { 203 String methodName = ""; 204 if( postMethodAnnotation != null ) { 205 methodName = postMethodAnnotation.method(); 206 } 207 if( methodName.equals("" )) { 208 methodName = method.getName(); 209 } 210 if( methodName.startsWith(FORM_POST_METHOD_NAME_PREFIX)) { 211 methodName = method.getName().substring(FORM_POST_METHOD_NAME_PREFIX.length()); 212 } 213 return methodName; 214 } 215 216 private String getStandardMethodName(Method method, DirectMethod methodAnnotation) { 217 String methodName = ""; 218 if( methodAnnotation != null ) { 219 methodName = methodAnnotation.method(); 220 } 221 if( methodName.equals("" )) { 222 methodName = method.getName(); 223 } 224 if( methodName.startsWith(STANDARD_METHOD_NAME_PREFIX)) { 225 methodName = method.getName().substring(STANDARD_METHOD_NAME_PREFIX.length()); 226 } 227 return methodName; 228 } 229 230 private void processPollMethod(RegisteredAction action, Method method, DirectPollMethod pollMethodAnnotation) { 231 String eventName = getEventName(method, pollMethodAnnotation); 232 233 RegisteredPollMethod poll = new RegisteredPollMethod( eventName, method, action.getActionClass()); 234 235 if( this.pollMethods.containsKey(poll.getName())) { 236 ApiConfigurationException ex = ApiConfigurationException.forPollEventAlreadyRegistered( poll ); 237 logger.fatal( ex.getMessage(), ex ); 238 throw ex; 239 } 240 241 // Register in the list of poll methods/events 242 this.pollMethods.put( poll.getName(), poll ); 243 this.currentApiPollMethods.add( poll ); 244 245 if( logger.isDebugEnabled() ) { 246 logger.debug( " - Registered new Poll Method. Event name: '" + poll.getName() + "'. Java method: '" + action.getActionClass().getName() + "." + method.getName() + "'" ); 247 } 248 } 249 250 private String getEventName(Method method, DirectPollMethod pollMethodAnnotation) { 251 String eventName = ""; 252 if( pollMethodAnnotation != null ) { 253 eventName = pollMethodAnnotation.event(); 254 } 255 if( eventName.equals("")) { 256 eventName = method.getName(); 257 } 258 if( eventName.startsWith(POLL_METHOD_NAME_PREFIX)) { 259 eventName = method.getName().substring(POLL_METHOD_NAME_PREFIX.length()); 260 } 261 return eventName; 262 } 263 264 public Registry( List<ApiConfiguration> apis ) { 265 assert apis != null; 266 267 for( ApiConfiguration api: apis ) { 268 if( this.apis.containsKey( api.getName() )) { 269 ApiConfigurationException ex = ApiConfigurationException.forApiAlreadyRegistered( api.getName()); 270 logger.fatal( ex.getMessage(), ex ); 271 throw ex; 272 } 273 274 this.currentApiActions = new ArrayList<RegisteredAction>(); 275 this.currentApiPollMethods = new ArrayList<RegisteredPollMethod>(); 276 List<Class<?>> actionClasses = api.getClasses(); 277 278 if( actionClasses.size() == 0 ) { 279 logger.warn( "There are no action classes to register"); 280 } 281 for( Class<?> cls : actionClasses ) { 282 assert cls != null; 283 registerActionClass(cls ); 284 } 285 286 RegisteredApi registeredApi = new RegisteredApi( api.getFullApiFileName(), api.getNamespace(), this.currentApiActions, this.currentApiPollMethods); 287 this.apis.put( api.getName(), registeredApi ); 288 } 289 } 290 291 /* package */ public RegisteredAction getAction( String actionName ) { 292 assert !StringUtils.isEmpty( actionName ); 293 assert isActionRegistered( actionName ); 294 295 RegisteredAction action = this.actionsByName.get( actionName ); 296 return action; 297 } 298 299 private boolean isActionRegistered(String actionName) { 300 assert !StringUtils.isEmpty( actionName ); 301 302 return this.actionsByName.containsKey( actionName ); 303 } 304 305 public List<RegisteredAction> getActions() { 306 return new ArrayList<RegisteredAction>( this.actionsByName.values() ); 307 } 308 309 public RegisteredPollMethod getPollEvent(String eventName) { 310 assert eventName != null; 311 312 return this.pollMethods.get(eventName); 313 } 314 315 public List<RegisteredPollMethod> getPollMethods() { 316 return new ArrayList<RegisteredPollMethod>(this.pollMethods.values()); 317 } 318 319 public List<RegisteredApi> getApis() { 320 return new ArrayList<RegisteredApi>(this.apis.values()); 321 } 322 323 public void updateJavascriptApiFiles(GlobalConfiguration globalConfiguration ) throws IOException { 324 assert globalConfiguration != null; 325 326 Map<String, StringBuilder> debugFileOutputs = new HashMap<String,StringBuilder>(); 327 Map<String, StringBuilder> standardFileOutputs = new HashMap<String,StringBuilder>(); 328 329 generateCode(debugFileOutputs, standardFileOutputs, globalConfiguration); 330 saveCode(debugFileOutputs, standardFileOutputs, globalConfiguration.getDebug()); 331 } 332 333 private void generateCode(Map<String, StringBuilder> debugFileOutputs, 334 Map<String, StringBuilder> standardFileOutputs, GlobalConfiguration globalConfiguration) { 335 for( RegisteredApi api : getApis() ) { 336 String fileName = api.getFullApiFileName(); 337 StringBuilder output = debugFileOutputs.get(fileName); 338 StringBuilder minifiedOutput = standardFileOutputs.get(fileName); 339 assert (output == null) == (minifiedOutput == null); 340 341 if( output == null) { 342 output = new StringBuilder(); 343 debugFileOutputs.put( fileName, output); 344 345 minifiedOutput = new StringBuilder(); 346 standardFileOutputs.put( fileName, minifiedOutput); 347 } 348 349 ApiCodeGenerator generator = new ApiCodeGenerator( globalConfiguration, api ); 350 generator.appendCode(output, false); 351 generator.appendCode(minifiedOutput, true); 352 } 353 } 354 355 private void saveCode(Map<String, StringBuilder> debugFileOutputs, 356 Map<String, StringBuilder> standardFileOutputs, boolean debug) throws IOException 357 { 358 for( String file : debugFileOutputs.keySet() ) { 359 String debugFileName = getDebugFileName(file); 360 String minifiedFileName = Minifier.getMinifiedFileName(file); 361 362 String debugCode = debugFileOutputs.get(file).toString(); 363 boolean apiHasChanged = fileNeedsUpdating(new File(file), debugCode); 364 if( apiHasChanged ) { 365 String standardCode = standardFileOutputs.get(file).toString(); 366 String minifiedCode = Minifier.minify(standardCode, file, debugCode.length()); 367 // If could not minify code, use debug code as "minified" code 368 if( minifiedCode == null ) { 369 logger.warn( "Unable to minify code: using Standard code for api file '" + minifiedFileName + "'."); 370 minifiedCode = standardCode; 371 } 372 String defaultCode = minifiedCode; 373 if( debug ) { 374 logger.info( "Debug mode: using Debug code for api file '" + file + "'."); 375 defaultCode = debugCode; 376 } 377 else { 378 logger.info( "Non debug mode: using Minified code for api file '" + file + "'."); 379 } 380 381 saveToFile( file, defaultCode ); 382 saveToFile( debugFileName, debugCode ); 383 saveToFile( minifiedFileName, minifiedCode ); 384 } 385 else { 386 if( logger.isDebugEnabled() ) { 387 logger.debug( "Api file '" + new File(file).getAbsolutePath() + '\'' + " is up to date: it was not rewritten."); 388 } 389 } 390 } 391 } 392 393 private void saveToFile( String fullFileName, String code ) throws IOException { 394 assert !StringUtils.isEmpty(fullFileName); 395 assert code != null; 396 397 File file = new File( fullFileName ); 398 if( fileNeedsUpdating(file, code) ) { 399 FileUtils.writeStringToFile(file, code); 400 if( logger.isDebugEnabled() ) { 401 logger.debug( "Api file generated: '" + file.getAbsolutePath() + "'"); 402 } 403 } 404 else { 405 if( logger.isDebugEnabled() ) { 406 logger.debug( "Api file '" + file.getAbsolutePath() + '\'' + " is up to date: it was not rewritten."); 407 } 408 } 409 } 410 411 private boolean fileNeedsUpdating(File file, String code) throws IOException { 412 if( file.exists()) { 413 String contents = FileUtils.readFileToString(file); 414 return !contents.equals( code ); 415 } 416 return true; 417 } 418 419 private String getDebugFileName( String file ) { 420 String result = file.replace( ".js", "-debug.js"); 421 return result; 422 } 423 }