001    /*
002     * Copyright © 2008, 2009 Pedro Agulló Soliveres.
003     * 
004     * This file is part of DirectJNgine.
005     *
006     * DirectJNgine is free software: you can redistribute it and/or modify
007     * it under the terms of the GNU General Public License as published by
008     * the Free Software Foundation, either version 3 of the License.
009     *
010     * DirectJNgine is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with DirectJNgine.  If not, see <http://www.gnu.org/licenses/>.
017     * 
018     * This software uses the ExtJs library (http://extjs.com), which is 
019     * distributed under the GPL v3 license (see http://extjs.com/license).
020     */
021    
022    package com.softwarementors.extjs.djn.config;
023    
024    import java.io.File;
025    import java.io.IOException;
026    import java.lang.reflect.Method;
027    import java.util.ArrayList;
028    import java.util.HashMap;
029    import java.util.List;
030    import java.util.Map;
031    
032    import org.apache.commons.io.FileUtils;
033    import org.apache.log4j.Logger;
034    
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.DirectMethod;
039    import com.softwarementors.extjs.djn.config.annotations.DirectPollMethod;
040    import com.softwarementors.extjs.djn.config.annotations.DirectPostMethod;
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        RegisteredAction action = createAction(actionClass );
069        String actionName = action.getName();
070        
071        if( isActionRegistered( actionName ) ) {
072          RegisteredAction existingAction = this.actionsByName.get( actionName );
073          ApiConfigurationException ex = ApiConfigurationException.forActionAlreadyRegistered(action, existingAction.getActionClass());
074          logger.fatal( ex.getMessage(), ex );
075          throw ex;
076        }
077        
078        this.actionsByClass.put( actionClass, action);
079        this.actionsByName.put( actionName, action );
080        this.currentApiActions.add( action );
081        return action;
082      }
083      
084      private RegisteredAction createAction(Class<?> actionClass) {
085        assert actionClass != null;
086        
087        RegisteredAction action = createActionFromAnnotation(actionClass);
088        Map<String, RegisteredMethod> methodsByName = createMethodsFromAnnotations(action);
089        action.setMethods( methodsByName );
090        
091        return action;
092      }
093    
094      private RegisteredAction createActionFromAnnotation(Class<?> actionClass) {
095        assert actionClass != null;
096        
097        DirectAction actionAnnotation = actionClass.getAnnotation(DirectAction.class);
098        String actionName = "";
099        if( actionAnnotation != null ) {
100          actionName = actionAnnotation.action();      
101        }
102        
103        if( actionName.equals("")) {
104          actionName = actionClass.getSimpleName();
105          logger.trace( "No 'action' name specified in the '" + DirectAction.class.getName() + "' annotation: using the class simple name ('" + actionName + "')." );
106        }
107        return new RegisteredAction( actionClass, actionName );
108      }
109      
110      private Map<String, RegisteredMethod> createMethodsFromAnnotations(RegisteredAction action) {
111        assert action != null;
112        
113        // *All* methods are candidates, including those in base classes, 
114        // even if the base class does not have a DirectAction annotation!
115        Method[] methods = action.getActionClass().getDeclaredMethods(); // Get private, protected and other methods!
116        
117        Map<String,RegisteredMethod> methodsByName = new HashMap<String,RegisteredMethod>();
118        for( Method method : methods ) {
119          DirectMethod methodAnnotation = method.getAnnotation(DirectMethod.class);
120          DirectPostMethod postMethodAnnotation = method.getAnnotation(DirectPostMethod.class);
121          if( methodAnnotation != null && postMethodAnnotation != null ) {
122            ApiConfigurationException ex = ApiConfigurationException.forMethodCantHaveDirectMethodAndDirectPostMethodAnnotationAtTheSameTime(action, method);
123            logger.fatal( ex.getMessage(), ex );
124            throw ex;
125          }
126          
127          // Process "normal" method
128          if( methodAnnotation != null || postMethodAnnotation != null) {
129            boolean formHandler = postMethodAnnotation != null;
130            String methodName = "";
131            if( methodAnnotation != null ) {
132              methodName = methodAnnotation.method(); 
133            }
134            else {
135              assert postMethodAnnotation != null; 
136              methodName = postMethodAnnotation.method();
137            }
138            if( methodName.equals("")) {
139              logger.trace( "No 'method' parameter specified in the '" + DirectMethod.class.getName() + "' or " + "'" + DirectPostMethod.class.getName() + "' annotation: using the Java method name ('" + methodName + "') as the method name" );
140              methodName = method.getName(); 
141            }
142            RegisteredMethod actionMethod =  new RegisteredMethod( action, methodName, method, formHandler );
143    
144            logger.trace( "Processing Java method: " + method.toString() );
145            
146            if( methodsByName.containsKey(methodName) ) {
147              ApiConfigurationException ex = ApiConfigurationException.forMethodAlreadyRegisteredInAction(actionMethod);
148              logger.fatal( ex.getMessage(), ex );
149              throw ex;
150            }
151            
152            methodsByName.put( methodName, actionMethod);      
153          }
154          
155    
156          DirectPollMethod pollMethodAnnotation = method.getAnnotation(DirectPollMethod.class);
157          if( (methodAnnotation != null || postMethodAnnotation != null) && pollMethodAnnotation != null) {
158            ApiConfigurationException ex = ApiConfigurationException.forPollMethodCantHaveDirectMethodOrDirectPostMethodAnnotationAtTheSameTime(action, method);
159            logger.fatal( ex.getMessage(), ex );
160            throw ex;
161          }
162          
163          // Process "poll" method
164          if( pollMethodAnnotation != null ) {
165            RegisteredPollMethod poll = createPollEventFromAnnotation( pollMethodAnnotation, method, action.getActionClass() );
166            if( this.pollMethods.containsKey(poll.getName())) {
167              ApiConfigurationException ex = ApiConfigurationException.forPollEventAlreadyRegistered( poll );
168              logger.fatal( ex.getMessage(), ex );
169              throw ex;
170            }
171            
172            // Register in the list of poll methods/events
173            this.pollMethods.put( poll.getName(), poll );
174            this.currentApiPollMethods.add( poll );
175          }
176        }
177        
178        return methodsByName;
179      }
180    
181      private RegisteredPollMethod createPollEventFromAnnotation(DirectPollMethod pollMethodAnnotation, Method method, Class<?> owningClass) {
182        assert pollMethodAnnotation != null;
183        assert method != null;
184        assert owningClass != null;
185        
186        String eventName = pollMethodAnnotation.event();
187        if( eventName.equals("")) {
188          eventName = method.getName();
189        }
190        
191        return new RegisteredPollMethod( eventName, method, owningClass);
192      }
193    
194      public Registry( List<ApiConfiguration> apis ) {
195        assert apis != null;
196        
197        for( ApiConfiguration api: apis ) {
198          if( this.apis.containsKey( api.getName() )) {
199            ApiConfigurationException ex = ApiConfigurationException.forApiAlreadyRegistered( api.getName());
200            logger.fatal( ex.getMessage(), ex );
201            throw ex;
202          }
203    
204          this.currentApiActions = new ArrayList<RegisteredAction>();
205          this.currentApiPollMethods = new ArrayList<RegisteredPollMethod>();
206          List<Class<?>> actionClasses = api.getClasses();
207          if( logger.isTraceEnabled() ) {      
208            for( Class<?> cls : actionClasses ) {
209              assert cls != null;
210              logger.trace( "Candidate class: '" + cls.getName() + "'");
211            }
212          }
213        
214          if( actionClasses.size() == 0 ) {
215            logger.info( "There are no action classes to register");
216          }
217          for( Class<?> cls : actionClasses ) {
218            assert cls != null;
219            registerActionClass(cls );
220          }
221          
222          RegisteredApi registeredApi = new RegisteredApi( api.getFullApiFileName(), api.getNamespace(), this.currentApiActions, this.currentApiPollMethods);
223          this.apis.put( api.getName(), registeredApi );
224        }
225      }
226    
227      /* package */ public RegisteredAction getAction( String actionName ) {
228        assert !StringUtils.isEmpty( actionName );
229        assert isActionRegistered( actionName );
230        
231        RegisteredAction action = this.actionsByName.get( actionName );
232        return action;
233      }
234    
235      private boolean isActionRegistered(String actionName) {
236        assert !StringUtils.isEmpty( actionName );
237    
238        return this.actionsByName.containsKey( actionName );
239      }
240    
241      public List<RegisteredAction> getActions() {
242        return new ArrayList<RegisteredAction>( this.actionsByName.values() );
243      }
244    
245      public RegisteredPollMethod getPollEvent(String eventName) {
246        assert eventName != null;
247        
248        return this.pollMethods.get(eventName);
249      }
250    
251      public List<RegisteredPollMethod> getPollMethods() {
252        return new ArrayList<RegisteredPollMethod>(this.pollMethods.values());
253      }
254      
255      public List<RegisteredApi> getApis() {
256        return new ArrayList<RegisteredApi>(this.apis.values());
257      }
258    
259      public void updateJavascriptApiFiles(GlobalConfiguration globalConfiguration ) throws IOException {
260        assert globalConfiguration != null;
261        
262        for( RegisteredApi api : getApis() ) {
263          ApiCodeGenerator generator = new ApiCodeGenerator( globalConfiguration, api );
264          StringBuilder result = new StringBuilder();
265          generator.appendCode(result);
266          saveToFile( api.getFullApiFileName(), result );
267        }
268      }
269    
270      private void saveToFile( String  fullFileName, StringBuilder result ) throws IOException {
271        assert !StringUtils.isEmpty(fullFileName);
272        
273        File file = new File( fullFileName );
274    
275        String code = result.toString();
276        if( fileNeedsUpdating(file, code) ) {
277          FileUtils.writeStringToFile(file, code);
278          logger.info( "Saved generated ExtJs Direct API file to '" + file.getAbsolutePath() + '\'');    
279        }
280        else {
281          logger.info( "File '" + file.getAbsolutePath() + '\'' + " is up to date: it was not rewritten.");          
282        }
283      }
284    
285      private boolean fileNeedsUpdating(File file, String code) throws IOException {
286        if( file.exists()) {
287          String contents = FileUtils.readFileToString(file);
288          return !contents.equals( code );
289        }
290        return true;
291      }
292      
293    
294    }