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    }