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.DirectFormPostMethod;
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 = createMethods(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 static final String POLL_METHOD_NAME_PREFIX = "djnpoll_";
111      private static final String FORM_POST_METHOD_NAME_PREFIX = "djnform_";
112      private static final String STANDARD_METHOD_NAME_PREFIX = "djn_";
113    
114      private Map<String, RegisteredMethod> createMethods(RegisteredAction action) {
115        assert action != null;
116        
117        // *All* methods are candidates, including those in base classes, 
118        // even if the base class does not have a DirectAction annotation!
119        Method[] methods = action.getActionClass().getDeclaredMethods(); // Get private, protected and other methods!
120        
121        Map<String,RegisteredMethod> methodsByName = new HashMap<String,RegisteredMethod>();
122        for( Method method : methods ) {
123          // Check if the kind of direct method -if any
124          DirectMethod methodAnnotation = method.getAnnotation(DirectMethod.class);
125          boolean isStandardMethod = methodAnnotation != null;
126          if( !isStandardMethod ) {
127            isStandardMethod = method.getName().startsWith(STANDARD_METHOD_NAME_PREFIX);
128          }
129          
130          DirectFormPostMethod postMethodAnnotation = method.getAnnotation(DirectFormPostMethod.class);
131          boolean isFormPostMethod = postMethodAnnotation != null;
132          if( !isFormPostMethod ) {
133            isFormPostMethod = method.getName().startsWith(FORM_POST_METHOD_NAME_PREFIX);  
134          }
135          
136          DirectPollMethod pollMethodAnnotation = method.getAnnotation(DirectPollMethod.class);
137          boolean isPollMethod = pollMethodAnnotation != null;
138          if( !isPollMethod ) {
139            isPollMethod = method.getName().startsWith( POLL_METHOD_NAME_PREFIX );
140          }
141          
142          // Check that a method is just of only one kind of method
143          if( isStandardMethod && isFormPostMethod ) {
144            ApiConfigurationException ex = ApiConfigurationException.forMethodCantBeStandardAndFormPostMethodAtTheSameTime(action, method);
145            logger.fatal( ex.getMessage(), ex );
146            throw ex;
147          }
148          if( (methodAnnotation != null || postMethodAnnotation != null) && isPollMethod) {
149            ApiConfigurationException ex = ApiConfigurationException.forPollMethodCantBeStandardOrFormPostMethodAtTheSameTime(action, method);
150            logger.fatal( ex.getMessage(), ex );
151            throw ex;
152          }
153    
154          // Process standard and form post methods together, as they are very similar
155          if( isStandardMethod || isFormPostMethod) {
156            String methodName = "";
157            if( isStandardMethod ) {
158              methodName = getStandardMethodName(method, methodAnnotation);
159            }
160            else {
161              methodName = getFormPostMethodName( method, postMethodAnnotation);
162            }
163            RegisteredMethod actionMethod =  new RegisteredMethod( action, methodName, method, isFormPostMethod );
164            
165            if( methodsByName.containsKey(methodName) ) {
166              ApiConfigurationException ex = ApiConfigurationException.forMethodAlreadyRegisteredInAction(actionMethod);
167              logger.fatal( ex.getMessage(), ex );
168              throw ex;
169            }
170            
171            methodsByName.put( methodName, actionMethod);
172          }
173                
174          // Process "poll" method
175          if( isPollMethod ) {
176            processPollMethod(action, method, pollMethodAnnotation);
177          }
178        }
179        
180        return methodsByName;
181      }
182    
183      private String getFormPostMethodName(Method method, DirectFormPostMethod postMethodAnnotation) {
184        String methodName;
185        if( postMethodAnnotation != null ) {
186          methodName = postMethodAnnotation.method();
187          if( methodName.equals("" )) {
188            methodName = method.getName(); 
189          }
190        }
191        else {
192          methodName = method.getName().substring(FORM_POST_METHOD_NAME_PREFIX.length());
193        }
194        return methodName;
195      }
196    
197      private String getStandardMethodName(Method method, DirectMethod methodAnnotation) {
198        String methodName;
199        if( methodAnnotation != null ) {
200          methodName = methodAnnotation.method();
201          if( methodName.equals("" )) {
202            methodName = method.getName(); 
203          }
204        }
205        else {
206          methodName = method.getName().substring(STANDARD_METHOD_NAME_PREFIX.length());
207        }
208        return methodName;
209      }
210    
211      private void processPollMethod(RegisteredAction action, Method method, DirectPollMethod pollMethodAnnotation) {
212        String eventName;
213        if( pollMethodAnnotation != null ) {
214          eventName = pollMethodAnnotation.event();
215          if( eventName.equals("")) {
216            eventName = method.getName();
217          }         
218        }
219        else {
220          eventName = method.getName().substring(POLL_METHOD_NAME_PREFIX.length());
221        }
222        
223        RegisteredPollMethod poll = new RegisteredPollMethod( eventName, method, action.getActionClass());
224        
225        if( this.pollMethods.containsKey(poll.getName())) {
226          ApiConfigurationException ex = ApiConfigurationException.forPollEventAlreadyRegistered( poll );
227          logger.fatal( ex.getMessage(), ex );
228          throw ex;
229        }
230        
231        // Register in the list of poll methods/events
232        this.pollMethods.put( poll.getName(), poll );
233        this.currentApiPollMethods.add( poll );
234      }
235    
236      public Registry( List<ApiConfiguration> apis ) {
237        assert apis != null;
238        
239        for( ApiConfiguration api: apis ) {
240          if( this.apis.containsKey( api.getName() )) {
241            ApiConfigurationException ex = ApiConfigurationException.forApiAlreadyRegistered( api.getName());
242            logger.fatal( ex.getMessage(), ex );
243            throw ex;
244          }
245    
246          this.currentApiActions = new ArrayList<RegisteredAction>();
247          this.currentApiPollMethods = new ArrayList<RegisteredPollMethod>();
248          List<Class<?>> actionClasses = api.getClasses();
249          if( logger.isTraceEnabled() ) {      
250            for( Class<?> cls : actionClasses ) {
251              assert cls != null;
252              logger.trace( "Candidate class: '" + cls.getName() + "'");
253            }
254          }
255        
256          if( actionClasses.size() == 0 ) {
257            logger.info( "There are no action classes to register");
258          }
259          for( Class<?> cls : actionClasses ) {
260            assert cls != null;
261            registerActionClass(cls );
262          }
263          
264          RegisteredApi registeredApi = new RegisteredApi( api.getFullApiFileName(), api.getNamespace(), this.currentApiActions, this.currentApiPollMethods);
265          this.apis.put( api.getName(), registeredApi );
266        }
267      }
268    
269      /* package */ public RegisteredAction getAction( String actionName ) {
270        assert !StringUtils.isEmpty( actionName );
271        assert isActionRegistered( actionName );
272        
273        RegisteredAction action = this.actionsByName.get( actionName );
274        return action;
275      }
276    
277      private boolean isActionRegistered(String actionName) {
278        assert !StringUtils.isEmpty( actionName );
279    
280        return this.actionsByName.containsKey( actionName );
281      }
282    
283      public List<RegisteredAction> getActions() {
284        return new ArrayList<RegisteredAction>( this.actionsByName.values() );
285      }
286    
287      public RegisteredPollMethod getPollEvent(String eventName) {
288        assert eventName != null;
289        
290        return this.pollMethods.get(eventName);
291      }
292    
293      public List<RegisteredPollMethod> getPollMethods() {
294        return new ArrayList<RegisteredPollMethod>(this.pollMethods.values());
295      }
296      
297      public List<RegisteredApi> getApis() {
298        return new ArrayList<RegisteredApi>(this.apis.values());
299      }
300    
301      public void updateJavascriptApiFiles(GlobalConfiguration globalConfiguration ) throws IOException {
302        assert globalConfiguration != null;
303        
304        for( RegisteredApi api : getApis() ) {
305          ApiCodeGenerator generator = new ApiCodeGenerator( globalConfiguration, api );
306          StringBuilder result = new StringBuilder();
307          generator.appendCode(result);
308          saveToFile( api.getFullApiFileName(), result );
309        }
310      }
311    
312      private void saveToFile( String  fullFileName, StringBuilder result ) throws IOException {
313        assert !StringUtils.isEmpty(fullFileName);
314        
315        File file = new File( fullFileName );
316    
317        String code = result.toString();
318        if( fileNeedsUpdating(file, code) ) {
319          FileUtils.writeStringToFile(file, code);
320          logger.info( "Saved generated ExtJs Direct API file to '" + file.getAbsolutePath() + '\'');    
321        }
322        else {
323          logger.info( "File '" + file.getAbsolutePath() + '\'' + " is up to date: it was not rewritten.");          
324        }
325      }
326    
327      private boolean fileNeedsUpdating(File file, String code) throws IOException {
328        if( file.exists()) {
329          String contents = FileUtils.readFileToString(file);
330          return !contents.equals( code );
331        }
332        return true;
333      }
334      
335    
336    }