PageRenderTime 46ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/WebServerDelegate.m

http://switchlist.googlecode.com/
Objective C | 401 lines | 293 code | 50 blank | 58 comment | 60 complexity | 794181e933e77e83178049c9c6d2ffb5 MD5 | raw file
  1. //
  2. //
  3. // WebServerDelegate.m
  4. // SwitchList
  5. //
  6. // Created by bowdidge on 11/20/10.
  7. //
  8. // Copyright (c)2010 Robert Bowdidge,
  9. // All rights reserved.
  10. //
  11. // Redistribution and use in source and binary forms, with or without
  12. // modification, are permitted provided that the following conditions
  13. // are met:
  14. // 1. Redistributions of source code must retain the above copyright
  15. // notice, this list of conditions and the following disclaimer.
  16. // 2. Redistributions in binary form must reproduce the above copyright
  17. // notice, this list of conditions and the following disclaimer in the
  18. // documentation and/or other materials provided with the distribution.
  19. //
  20. // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  21. // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  22. // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  23. // ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  24. // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  25. // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  26. // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  27. // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  28. // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  29. // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  30. // SUCH DAMAGE.
  31. //
  32. #import <Foundation/Foundation.h>
  33. #import "WebServerDelegate.h"
  34. #import "CarType.h"
  35. #import "EntireLayout.h"
  36. #import "FreightCar.h"
  37. #import "GlobalPreferences.h"
  38. #import "HTMLSwitchlistRenderer.h"
  39. #import "InduYard.h"
  40. #import "Industry.h"
  41. #import "MGTemplateEngine/MGTemplateEngine.h"
  42. #import "MGTemplateEngine/ICUTemplateMatcher.h"
  43. #import "NSFileManager+DirectoryLocations.h"
  44. #import "Place.h"
  45. #import "SwitchListAppDelegate.h"
  46. #import "SwitchListDocument.h"
  47. #import "SwitchListFilters.h"
  48. #import "SimpleHTTPServer/SimpleHTTPServer.h"
  49. #import "SimpleHTTPServer/SimpleHTTPConnection.h"
  50. #import "Yard.h"
  51. #include <regex.h> // For pattern matching on IP address.
  52. static const int HTTP_OK = 200;
  53. static const int HTTP_FORBIDDEN = 403;
  54. static const int HTTP_NOT_FOUND = 404;
  55. const int DEFAULT_SWITCHLIST_PORT = 20000;
  56. BOOL IsValidIPAddress(NSString *potentialAddress) {
  57. regex_t patternCompiled;
  58. BOOL isValidPattern = NO;
  59. int ret;
  60. ret = regcomp(&patternCompiled, "^[0-9]+.[0-9]+.[0-9]+.[0-9]+$", REG_EXTENDED);
  61. if (ret != 0) {
  62. NSLog(@"Regex pattern for detecting appropriate IP address for server returned %d\n", ret);
  63. exit(1);
  64. }
  65. ret = regexec(&patternCompiled, [potentialAddress UTF8String], 0, NULL, 0);
  66. if (!ret) {
  67. isValidPattern = YES;
  68. } else if (ret == REG_NOMATCH) {
  69. isValidPattern = NO;
  70. } else {
  71. char message[100];
  72. regerror(ret, &patternCompiled, message, sizeof(message));
  73. printf("Regex match failed: %s\n", message);
  74. isValidPattern = NO;
  75. }
  76. regfree(&patternCompiled);
  77. return isValidPattern;
  78. }
  79. NSString *CurrentHostname() {
  80. NSArray *ipAddresses = [[NSHost currentHost] addresses];
  81. for (NSString *address in ipAddresses) {
  82. if (IsValidIPAddress(address)) {
  83. return address;
  84. }
  85. }
  86. // TODO(bowdidge): Find better way to warn when no addresses are valid.
  87. return @"127.0.0.1";
  88. }
  89. @implementation WebServerDelegate
  90. // For mocking.
  91. - (id) initWithServer: (SimpleHTTPServer*) server withBundle: (NSBundle*) bundle withRenderer: (HTMLSwitchlistRenderer*) renderer {
  92. [super init];
  93. server_ = [server retain];
  94. htmlRenderer_ = [renderer retain];
  95. [htmlRenderer_ setTemplate: DEFAULT_SWITCHLIST_TEMPLATE];
  96. if (server_) {
  97. NSLog(@"Started!");
  98. } else {
  99. NSLog(@"Problems starting server!");
  100. }
  101. return self;
  102. }
  103. // Preferred constructor.
  104. - (id) init {
  105. HTMLSwitchlistRenderer *htmlRenderer = [[HTMLSwitchlistRenderer alloc] initWithBundle: [NSBundle mainBundle]];
  106. [htmlRenderer autorelease];
  107. return [self initWithServer: [[[SimpleHTTPServer alloc] initWithTCPPort: DEFAULT_SWITCHLIST_PORT delegate:self] autorelease]
  108. withBundle: [NSBundle mainBundle]
  109. withRenderer: htmlRenderer];
  110. }
  111. - (void) dealloc {
  112. [server_ stopResponding];
  113. [server_ release];
  114. [htmlRenderer_ release];
  115. [super dealloc];
  116. }
  117. // Query stops.
  118. - (void) stopProcessing {
  119. NSLog(@"Stop!");
  120. }
  121. // Shut down server.
  122. - (void) stopResponding {
  123. [server_ stopResponding];
  124. }
  125. // Change the default switchlist template to the named one.
  126. // If templateName is Handwritten or is nil, then use the default template.
  127. - (void) setTemplate: (NSString*) templateName {
  128. [htmlRenderer_ setTemplate: templateName];
  129. }
  130. - (void) processError: (NSURL *) badURL {
  131. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  132. message: [NSString stringWithFormat: @"Unknown URL %@", [badURL path]]];
  133. }
  134. - (void) processRequestForSwitchlistCSS {
  135. NSString *cssFile = [htmlRenderer_ filePathForSwitchlistCSS];
  136. if (!cssFile) {
  137. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  138. message: [NSString stringWithFormat: @"Unknown URL switchlist.css"]];
  139. return;
  140. }
  141. NSData *data = [NSData dataWithContentsOfURL: [NSURL fileURLWithPath: cssFile]];
  142. [server_ replyWithData:data MIMEType: @"text/css"];
  143. }
  144. - (void) processRequestForSwitchlistIPhoneCSS {
  145. NSString *cssFile = [htmlRenderer_ filePathForSwitchlistIPhoneCSS];
  146. if (!cssFile) {
  147. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  148. message: [NSString stringWithFormat: @"Unknown URL switchlist.css"]];
  149. return;
  150. }
  151. NSData *data = [NSData dataWithContentsOfURL: [NSURL fileURLWithPath: cssFile]];
  152. [server_ replyWithData:data MIMEType: @"text/css"];
  153. }
  154. - (void) processRequestForSwitchlistIPadCSS {
  155. NSString *cssFile = [htmlRenderer_ filePathForSwitchlistIPadCSS];
  156. if (!cssFile) {
  157. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  158. message: [NSString stringWithFormat: @"Unknown URL switchlist.css"]];
  159. return;
  160. }
  161. NSData *data = [NSData dataWithContentsOfURL: [NSURL fileURLWithPath: cssFile]];
  162. [server_ replyWithData:data MIMEType: @"text/css"];
  163. }
  164. - (void) processRequestForDefaultCSS: (NSString*) filePrefix {
  165. NSString *cssFile = [htmlRenderer_ filePathForDefaultCSS: filePrefix];
  166. if (!cssFile) {
  167. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  168. message: [NSString stringWithFormat: @"Unknown URL %@.css", filePrefix]];
  169. return;
  170. }
  171. NSData *data = [NSData dataWithContentsOfURL: [NSURL fileURLWithPath: cssFile]];
  172. [server_ replyWithData:data MIMEType: @"text/css"];
  173. }
  174. - (void) processRequestForLayout: (SwitchListDocument*) document train: (NSString*) trainName forIPhone: (BOOL) isIPhone {
  175. // TODO(bowdidge): Current document is nil whenever not active.
  176. EntireLayout *layout = [document entireLayout];
  177. ScheduledTrain *train = [layout trainWithName: trainName];
  178. [server_ replyWithStatusCode: HTTP_OK
  179. message: [htmlRenderer_ renderSwitchlistForTrain: train layout: layout iPhone: isIPhone]];
  180. }
  181. // Generates HTML response for the car list for the names layout.
  182. - (void) processRequestForCarListForLayout: (SwitchListDocument*) document {
  183. // TODO(bowdidge): Current document is nil whenever not active.
  184. EntireLayout *layout = [document entireLayout];
  185. NSString *message = [htmlRenderer_ renderCarlistForLayout: layout];
  186. [server_ replyWithStatusCode: HTTP_OK message: message];
  187. }
  188. // Returns HTML for industry list, showing the cars at each industry
  189. - (void) processRequestForIndustryListForLayout: (SwitchListDocument*) document {
  190. EntireLayout *layout = [document entireLayout];
  191. NSString *message = [htmlRenderer_ renderIndustryListForLayout: layout];
  192. [server_ replyWithStatusCode: HTTP_OK message: message];
  193. }
  194. // Marks the given train as completed, and moves cars to final locations.
  195. - (void) processCompleteTrain: (NSString*) trainName forLayout: (SwitchListDocument*) document {
  196. ScheduledTrain *train = [[document entireLayout] trainWithName: trainName];
  197. if (!train) {
  198. [server_ replyWithStatusCode: HTTP_OK message: [NSString stringWithFormat: @"Unknown train %@", trainName]];
  199. return;
  200. }
  201. [[document layoutController] completeTrain: train];
  202. [server_ replyWithStatusCode: HTTP_OK message: [NSString stringWithFormat: @"Train %@ marked completed!", trainName]];
  203. }
  204. // Given parameters to changeCarLocation, updates database.
  205. - (void) processChangeLocationForLayout: (SwitchListDocument*) document car: (NSString*) carName location: (NSString*) locationName {
  206. carName = [carName stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
  207. EntireLayout *entireLayout = [document entireLayout];
  208. FreightCar *fc = [entireLayout freightCarWithName: carName];
  209. InduYard *location = [entireLayout industryOrYardWithName: locationName];
  210. if (!fc) {
  211. [server_ replyWithStatusCode: HTTP_OK
  212. message: [NSString stringWithFormat: @"No such freight car %@", carName]];
  213. return;
  214. }
  215. if (!location) {
  216. [server_ replyWithStatusCode: HTTP_OK
  217. message: [NSString stringWithFormat: @"No such location %@", locationName]];
  218. return;
  219. }
  220. [fc setCurrentLocation: location];
  221. [server_ replyWithStatusCode: HTTP_OK
  222. message: @"OK"];
  223. }
  224. - (void) processRequestForLayout: (SwitchListDocument*) document {
  225. NSString *message = [htmlRenderer_ renderLayoutPageForLayout: [document entireLayout]];
  226. [server_ replyWithStatusCode: HTTP_OK message: message];
  227. }
  228. - (SwitchListDocument*) layoutWithName: (NSString*) layoutName {
  229. if ([layoutName isEqualToString: @"untitled"]) {
  230. layoutName = @"";
  231. }
  232. NSDocumentController *controller = [NSDocumentController sharedDocumentController];
  233. NSArray *allDocuments = [controller documents];
  234. for (SwitchListDocument *d in allDocuments) {
  235. if ([[[d entireLayout] layoutName] isEqualToString: layoutName]) {
  236. return d;
  237. }
  238. }
  239. return nil;
  240. }
  241. // Generates a page that is a redirect to the provided URL.
  242. - (void) replyWithRedirectTo: (NSString*) dest {
  243. NSMutableString *message = [NSMutableString string];
  244. [message appendString: @"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n"
  245. @"<html><head><title>SwitchList</title>"];
  246. [message appendFormat: @"<meta http-equiv=\"REFRESH\" content=\"0;url=%@\"></HEAD>", dest];
  247. [message appendString: @"<BODY></body></html>\n"];
  248. [server_ replyWithStatusCode: HTTP_OK message: message];
  249. }
  250. - (void) showAllLayouts {
  251. [server_ replyWithStatusCode: HTTP_OK message: [htmlRenderer_ renderLayoutsPage]];
  252. }
  253. // URLs should be of form:
  254. // http://localhost:20000/ -- show list of layouts
  255. // http://localhost:20000/get?layout="xxx" -- show list of trains on layout xxx.
  256. // http://localhost:20000/get?layout="xxx"&train="yyyy" - show details on train yyy on layout xxx.
  257. // http://localhost:20000/get?layout="xxx"&carList=1 -- show list of freight cars, and allow changing locations.
  258. // http://localhost:20000/get?layout="xxx"&industryList=1 -- show list of freight cars, and allow changing locations.
  259. //
  260. // http://localhost:20000/setCarLocation?layout="xxx"&car="xxx"&location="xxx" -- change car's location.
  261. // TODO(bowdidge): Perhaps switch layout/train query to being part of the path, then use queries to set'
  262. // values? That would also make it easier to do a car detail view.
  263. - (void) processURL: (NSURL*) url connection: (SimpleHTTPConnection*) conn userAgent: (NSString*) userAgent {
  264. NSLog(@"Process %@", url);
  265. NSString *urlClean = [[url query] stringByReplacingOccurrencesOfString: @"%20" withString: @" "];
  266. // If connecting from an iPhone, the UserAgent should contain '(iPhone;' somewhere.
  267. BOOL isIPhone = [userAgent rangeOfString: @"iPhone"].location != NSNotFound;
  268. if ([[url path] isEqualToString: @"/switchlist.css"]) {
  269. [self processRequestForSwitchlistCSS];
  270. return;
  271. } else if ([[url path] isEqualToString: @"/switchlist-iphone.css"]) {
  272. [self processRequestForSwitchlistIPhoneCSS];
  273. return;
  274. } else if ([[url path] isEqualToString: @"/switchlist-ipad.css"]) {
  275. [self processRequestForSwitchlistIPadCSS];
  276. return;
  277. }
  278. if ([[url path] isEqualToString: @"/builtin-switchlist.css"]) {
  279. NSLog(@"In builtin-switchlist.css");
  280. [self processRequestForDefaultCSS: @"builtin-switchlist"];
  281. return;
  282. } else if ([[url path] isEqualToString: @"/builtin-switchlist-iphone.css"]) {
  283. [self processRequestForDefaultCSS: @"builtin-switchlist-iphone"];
  284. return;
  285. } else if ([[url path] isEqualToString: @"/builtin-switchlist-ipad.css"]) {
  286. [self processRequestForDefaultCSS: @"builtin-switchlist-ipad"];
  287. return;
  288. }
  289. NSArray *queryTerms = [urlClean componentsSeparatedByString: @"&"];
  290. NSMutableDictionary *query = [NSMutableDictionary dictionary];
  291. for (NSString *item in queryTerms) {
  292. NSArray *queryPair = [item componentsSeparatedByString: @"="];
  293. if ([queryPair count] == 2) {
  294. [query setObject: [queryPair lastObject] forKey: [queryPair objectAtIndex: 0]];
  295. } else if ([queryPair count] == 1) {
  296. [query setObject: [NSNumber numberWithBool: true] forKey: [queryPair objectAtIndex: 0]];
  297. }
  298. }
  299. // For debugging: allow specifying "iphone=1" in URL to get iPhone UI.
  300. if ([query objectForKey: @"iphone"]) {
  301. isIPhone = YES;
  302. }
  303. if ([[url path] hasPrefix: @"/completeTrain"]) {
  304. NSString *train = [query objectForKey: @"train"];
  305. NSString *layout = [query objectForKey: @"layout"];
  306. SwitchListDocument *document = [self layoutWithName: layout];
  307. if (!document) {
  308. [server_ replyWithStatusCode: HTTP_OK
  309. message: [NSString stringWithFormat: @"No layout named %@.", layout]];
  310. return;
  311. }
  312. [self processCompleteTrain: train forLayout: document];
  313. return;
  314. } else if ([[url path] hasPrefix: @"/setCarLocation"]) {
  315. NSString *car = [query objectForKey: @"car"];
  316. NSString *location = [query objectForKey: @"location"];
  317. NSString *layout = [query objectForKey: @"layout"];
  318. SwitchListDocument *document = [self layoutWithName: layout];
  319. if (!document) {
  320. [server_ replyWithStatusCode: HTTP_OK
  321. message: [NSString stringWithFormat: @"No layout named %@.", layout]];
  322. return;
  323. }
  324. [self processChangeLocationForLayout: document car: car location: location];
  325. return;
  326. } else if ([[url path] isEqualToString: @"/get"]) {
  327. NSString *layoutName = [query objectForKey: @"layout"];
  328. SwitchListDocument *document = [self layoutWithName: layoutName];
  329. if (document == nil) {
  330. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  331. message: [NSString stringWithFormat: @"No such layout: '%@'.", layoutName]];
  332. return;
  333. }
  334. if ([query objectForKey: @"train"] != nil) {
  335. [self processRequestForLayout: document train: [query objectForKey: @"train"] forIPhone: isIPhone];
  336. } else if ([query objectForKey: @"carList"] != nil) {
  337. [self processRequestForCarListForLayout: document];
  338. } else if ([query objectForKey: @"industryList"] != nil) {
  339. [self processRequestForIndustryListForLayout: document];
  340. } else {
  341. // Default to showing layout.
  342. [self processRequestForLayout: document];
  343. }
  344. } else if ([[url path] isEqualToString: @"/"]) {
  345. [self showAllLayouts];
  346. } else {
  347. [server_ replyWithStatusCode: HTTP_NOT_FOUND
  348. message: [NSString stringWithFormat: @"Unknown path: '%@'.", [url path]]];
  349. }
  350. }
  351. @end