001package votorola.a; // Copyright 2007-2010, 2012, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. 002 003import java.io.*; 004import java.net.*; 005import java.util.*; 006import java.util.concurrent.locks.*; 007import java.util.logging.*; 008import java.util.regex.*; 009import votorola.a.count.*; 010import votorola.a.response.*; 011import votorola.g.*; 012import votorola.g.lang.*; 013import votorola.g.logging.*; 014import votorola.g.option.*; 015import votorola.g.script.*; 016 017 018/** A facility for voters to access and maintain a category of data on a vote-server. 019 */ 020public @ThreadRestricted("holds lock()") abstract class VoterService 021{ 022 023 024 /** Partially creates a VoterService. To complete it, call {@linkplain 025 * #init(ArrayList) init}(responderList). 026 */ 027 protected VoterService( VoteServer.Run _vsRun, final ConstructionContext cc ) 028 { 029 vsRun = _vsRun; 030 constructionContext = cc; 031 032 name = cc.name; 033 034 if( vsRun.isSingleThreaded() ) lock = vsRun.singleServiceLock(); 035 else lock = new ReentrantLock(); 036 } 037 038 039 040 /** @param responderList listing only the service-specific responders; 041 * the general responders will be added by this method. 042 */ 043 protected @ThreadRestricted("constructor") final void init( 044 final ArrayList<CommandResponder> responderList ) 045 { 046 responderList.add( new CR_Hello( VoterService.this ) ); 047 responderList.add( new CR_Help( VoterService.this )); 048 responderList.add( new CR_Version( VoterService.this )); 049 // responderList.trimToSize(); 050 // responders = Collections.unmodifiableList( responderList ); 051 responderArray = new CommandResponder[responderList.size()]; 052 responderList.toArray( responderArray ); 053 054 final int mapCapacity = (int)((responderList.size() + 1) / 0.75f) + 1; 055 respondersByClassName = new HashMap<String,CommandResponder>( mapCapacity ); 056 CommandResponder duplicate; 057 for( CommandResponder responder: responderList ) 058 { 059 logger.finer( "adding responder for '" + name + "': " + responder.getClass().getName() ); 060 duplicate = respondersByClassName.put( responder.getClass().getName(), responder ); 061 assert duplicate == null : "single responder of class:" + responder.getClass(); 062 } 063 } 064 065 066 067 // - V o t e r - S e r v i c e -------------------------------------------------------- 068 069 070 /** Looks up the responder of the specified command, and sends the command 071 * to it. Or, if the look-up fails, replies that the command is unrecognized. 072 * 073 * @param argArray an array containing the command name and arguments, 074 * per CommandResponder.respond(argv,session). 075 * @return any soft exception, per CommandResponder.respond(argv,session); 076 * or null if none occured. 077 * 078 * @see CommandResponder#respond(String[],CommandResponder.Session) 079 */ 080 public Exception dispatch( final String[] argArray, 081 final CommandResponder.Session commandSession ) 082 { 083 return dispatch( argArray, commandSession, responderForCommand( argArray, commandSession )); 084 } 085 086 087 088 /** Sends a command to its responder, if one is specified. Or, if none is specified, 089 * replies that the command is unrecognized. 090 * 091 * @param argArray an array containing the command name and arguments, 092 * per CommandResponder.respond(argv,session). 093 * @param responder the responder for the command, or null if there is none. 094 * 095 * @return any soft exception, per CommandResponder.respond(argv,session); or 096 * null if none occured. 097 * 098 * @see #responderForCommand(String[],CommandResponder.Session) 099 * @see CommandResponder#respond(String[],CommandResponder.Session) 100 */ 101 public final Exception dispatch( final String[] argArray, 102 final CommandResponder.Session commandSession, final CommandResponder responder ) 103 { 104 assert lock.isHeldByCurrentThread(); 105 if( responder != null ) return responder.respond( argArray, commandSession ); 106 107 final ReplyBuilder replyB = commandSession.replyBuilder(); 108 final String commandName = argArray[0]; 109 replyB.lappend( "a.VoterService.unrecognized(1)", commandName ); 110 final String unrecognizedHelpKey = "a.VoterService.unrecognizedHelp"; 111 if( !commandSession.containsKey( unrecognizedHelpKey )) // not yet prompted 112 { 113 commandSession.put( unrecognizedHelpKey, Boolean.TRUE ); 114 final CommandResponder help = responderByClassName( CR_Help.class.getName() ); 115 if( help != null ) 116 { 117 replyB.append( " " ); 118 replyB.lappendlnn( unrecognizedHelpKey ); 119 replyB.indent( 4 ); 120 replyB.append( help.commandName( commandSession )); 121 replyB.exdent( 4 ); 122 } 123 } 124 replyB.appendlnn(); 125 return null; 126 } 127 128 129 130 /** Responds to a help command on behalf of the nominal responder, per {@linkplain 131 * CR_Help#respond(String[],Session) respond}(argv,session). 132 */ 133 public Exception help( final String[] argv, final CommandResponder.Session session ) 134 { 135 helpA( session ); 136 helpB( session ); 137 helpC( session ); 138 return null; 139 } 140 141 142 143 /** Answers whether the named service is (or would be) a non-poll service. Currently 144 * the names of non-poll services always begin with a lowercase letter, whereas the 145 * names of polls never do. This is not guaranteed to hold in future, but you are 146 * safe so long as this is your test method. 147 */ 148 public static boolean isNonPoll( String name ) 149 { 150 return Character.isLowerCase( name.codePointAt( 0 )); 151 } 152 153 154 155 /** Returns the thread access lock for this service. Locking order: first lock the 156 * poll, then lock the trustserver. 157 * 158 * @see VoteServer.Run#singleServiceLock() 159 */ 160 public final ReentrantLock lock() { return lock; } 161 162 163 protected final ReentrantLock lock; 164 165 166 167 /** The local name of this service. It must be unique among all voter services of the 168 * vote-server. It must never change. 169 * 170 * @see #NAME_MAX_LENGTH 171 * @see #NAME_PATTERN 172 */ 173 public @ThreadSafe final String name() { return name; } 174 175 176 protected final String name; 177 178 179 180 /** The maximum length of a service name. 181 * 182 * @see #name() 183 * @see <a href='http://reluk.ca/w/Category:Poll' 184 * >zelea.com/w/Category:Poll</a> 185 */ 186 public static final int NAME_MAX_LENGTH = 50; 187 188 189 190 /** The allowable pattern of a service name. The name may contain the ASCII letters 191 * (A-Z, a-z), digits (0-9), and the punctuation characters underscore (_), slash 192 * (/), period (.) and dash (-). It must begin with a letter or an underscore, and 193 * must not end with a slash. Double slashes are not allowed. 194 * 195 * @see #name() 196 * @see <a href='http://reluk.ca/w/Category:Poll' 197 * >zelea.com/w/Category:Poll</a> 198 */ 199 public static final Pattern NAME_PATTERN = 200 Pattern.compile( "[A-Za-z_](?:/?[A-Za-z0-9_.\\-])*" ); // escaping the dash (\\-) is actually enough, no need to place it at end 201 202 203 204 /** The vote-server run, in which this service is provided. 205 */ 206 public @ThreadSafe final VoteServer.Run vsRun() { return vsRun; } 207 208 209 protected final VoteServer.Run vsRun; 210 211 212 213 /** Returns the responder of a particular class name, or null if there is none. 214 */ 215 public final CommandResponder responderByClassName( String className ) // by class name, rather than class, as a convenience because most lookups are by strings retrieved from the localized resource bundle 216 { 217 assert lock.isHeldByCurrentThread(); 218 return respondersByClassName.get( className ); 219 } 220 221 222 /** Map of responders, keyed by class name. 223 */ 224 private Map<String,CommandResponder> respondersByClassName; // final after init() 225 226 227 228 /** Returns the responder for the specified command, or null if there is none. 229 * 230 * @param argArray array of command name and arguments, 231 * per CommandResponder.respond(argv,session). 232 */ 233 public final CommandResponder responderForCommand( final String[] argArray, 234 final CommandResponder.Session commandSession ) 235 { 236 assert lock.isHeldByCurrentThread(); 237 final String commandName = argArray[0]; 238 String responderClassName = null; 239 try 240 { 241 responderClassName = commandSession.replyBuilder().bundle().getString( 242 "a.VoterService.className.noTrans(" + commandName + ")" ); 243 } 244 catch( MissingResourceException x ) { logger.finer( "no such command class exists: " + x ); } 245 246 CommandResponder responder = null; 247 if( responderClassName != null ) 248 { 249 responder = responderByClassName( responderClassName ); 250 if( responder == null ) logger.finer( "service does not support command class '" + responderClassName + "'" ); 251 } 252 253 return responder; 254 } 255 256 257 258 /** An array of all responders. 259 */ 260 // public final List<CommandResponder> responders() { return responders; } 261 public final CommandResponder[] responders() { return responderArray.clone(); } 262 263 264 // private List<CommandResponder> responders; 265 private CommandResponder[] responderArray; // final after init() 266 267 268 269 /** The directory containing this service's configuration files. 270 */ 271 public @ThreadSafe final File serviceDirectory() 272 { 273 return startupConfigurationFile().getParentFile(); 274 } 275 276 277 278 /** The startup configuration file for this service. The language is JavaScript. 279 * There are restrictions on the {@linkplain votorola.g.script.JavaScriptIncluder 280 * character encoding}. 281 */ 282 public abstract @ThreadSafe File startupConfigurationFile(); 283 284 285 286 /** A short description that summarizes this service. 287 * 288 * @see <a href='http://reluk.ca/w/Property:Short_description' 289 * >Pollwiki Property:Short_description</a> 290 */ 291 public abstract String summaryDescription(); 292 293 294 295 /** The title of this service in wiki-style title case. In English, that typically 296 * means only the first letter of the leading word is capitalized. 297 */ 298 public abstract String title(); 299 300 301 302 // - O b j e c t ---------------------------------------------------------------------- 303 304 305 /** Returns true iff o is a voter service of the same class with the same {@linkplain 306 * #name() name}. 307 */ 308 public @Override @ThreadSafe final boolean equals( final Object o ) 309 { 310 // cf. PollService.compareTo 311 if( o == null || !getClass().equals( o.getClass() )) return false; 312 313 return name.equals( ((VoterService)o).name ); 314 } 315 316 317 318 // public @Override final int hashCode() { return serviceEmail.hashCode(); } 319 320 321 322 /** Returns the service {@linkplain #name() name}. 323 */ 324 public @Override @ThreadSafe final String toString() { return name(); } 325 326 327 328 // ==================================================================================== 329 330 331 /** A context for configuring a {@linkplain VoterService voter serivce}. 332 */ 333 public static @ThreadSafe abstract class ConstructionContext 334 { 335 336 protected ConstructionContext( String name, final JavaScriptIncluder s ) 337 throws IllegalNameException 338 { 339 this( name, s, NAME_PATTERN ); 340 } 341 342 343 /** @param namePattern the allowable pattern, which may restrict but must not 344 * extend {@linkplain #NAME_PATTERN NAME_PATTERN}. 345 */ 346 protected ConstructionContext( String _name, final JavaScriptIncluder s, 347 final Pattern namePattern ) throws IllegalNameException 348 { 349 startupConfigurationFile = s.scriptFile(); 350 name = _name; 351 352 if( name.length() > NAME_MAX_LENGTH ) throw new IllegalNameException( "service name \"" + name + "\" exceeds maximum length " + NAME_MAX_LENGTH ); 353 354 final Matcher m = namePattern.matcher( name ); 355 if( !m.matches() ) throw new IllegalNameException( "service name \"" + name + "\" does not match allowable pattern: " + namePattern ); 356 357 } 358 359 360 // -------------------------------------------------------------------------------- 361 362 363 /** @see VoterService#startupConfigurationFile() 364 */ 365 public final File startupConfigurationFile() { return startupConfigurationFile; } 366 367 368 private final File startupConfigurationFile; 369 370 371 /** @see VoterService#name() 372 */ 373 public final String name() { return name; } 374 375 376 private final String name; 377 378 379 } 380 381 382 383 // ==================================================================================== 384 385 386 /** Thrown when a service with an illegal name is requested. 387 * 388 * @see VoterService#name() 389 */ 390 public static final class IllegalNameException extends VotorolaRuntimeException 391 { 392 public IllegalNameException( String message ) { super( message ); } 393 } 394 395 396 397 // ==================================================================================== 398 399 400 /** Thrown when an unknown voter service is requested. 401 */ 402 public static final class NoSuchServiceException extends MisconfigurationException 403 { 404 405 public NoSuchServiceException( String message, File filename ) 406 { 407 super( message, filename ); 408 } 409 410 } 411 412 413 414//// P r i v a t e /////////////////////////////////////////////////////////////////////// 415 416 417 /** Stored as a convenience for subclass initialization, may be nulled afterwards by 418 * subclass. 419 */ 420 protected ConstructionContext constructionContext; 421 422 423 424 protected final void helpA( final CommandResponder.Session session ) 425 { 426 helpA_1( session ); 427 helpA_2( session ); 428 helpA_3( session ); 429 } 430 431 432 protected final void helpA_1( final CommandResponder.Session session ) 433 { 434 assert lock.isHeldByCurrentThread(); 435 final ReplyBuilder replyB = session.replyBuilder(); 436 replyB.setWrapping( false ); 437 final String title = title(); 438 replyB.appendln( title ); 439 for( int c = title.length(); c > 0; --c ) replyB.append( '=' ); 440 replyB.appendlnn(); 441 442 replyB.setWrapping( true ); 443 } 444 445 446 protected void helpA_2( final CommandResponder.Session session ) 447 { 448 assert lock.isHeldByCurrentThread(); 449 final ReplyBuilder replyB = session.replyBuilder(); 450 replyB.setWrapping( false ); 451 replyB.indent( 4 ); 452 replyB.lappend( "a.VoterService.help.reply.summary(1,2)", 453 session.bunA().l( "a.serviceType(" + getClass().getName() + ")" ), 454 session.voterInterface().serviceAccessDescriptor( VoterService.this )); 455 replyB.exdent( 4 ).appendlnn(); 456 replyB.setWrapping( true ); 457 } 458 459 460 protected final void helpA_3( final CommandResponder.Session session ) 461 { 462 assert lock.isHeldByCurrentThread(); 463 session.replyBuilder().appendlnn( summaryDescription() ); 464 } 465 466 467 468 protected final void helpB( final CommandResponder.Session session ) 469 { 470 assert lock.isHeldByCurrentThread(); 471 final ReplyBuilder replyB = session.replyBuilder(); 472 replyB.lappendlnn( "a.VoterService.help.reply.body" ); 473 replyB.indent( 4 ); 474 replyB.lappend( "a.VoterService.help.reply.body-legend" ); 475 replyB.exdent( 4 ).appendlnn(); 476 } 477 478 479 480 protected final void helpC( final CommandResponder.Session session ) 481 { 482 assert lock.isHeldByCurrentThread(); 483 final ReplyBuilder replyB = session.replyBuilder(); 484 final CommandResponder[] responderArrayCopy = responders(); 485 Arrays.sort( responderArrayCopy, session.new ResponderNameComparator() ); 486 for( CommandResponder responder: responderArrayCopy ) 487 { 488 String title = responder.commandName( session ); 489 replyB.appendln( title ); 490 for( int c = title.length(); c > 0; --c ) replyB.append( '-' ); 491 replyB.append( '\n' ); 492 replyB.indent( 4 ); 493 responder.help( session ); 494 replyB.exdent( 4 ); 495 } 496 497 } 498 499 500 501 private static final Logger logger = LoggerX.i( VoterService.class ); 502 503 504}