[XCode] Building Universal iOS Frameworks

Here’s an excellent little project that I stumbled across not too long ago:

https://github.com/kstenerud/iOS-Universal-Framework

This project provides a single installation shell script that will add a new Project Template to XCode, one that can be used to build universal iOS frameworks. Now this wouldn’t be a big deal (and in all rights, shouldn’t be a big deal), if not for the fact that for some unspecified and frustrating reason, XCode has no built-in ability to create such frameworks. This means that up until recently if you had an iOS library that you wanted to use across many projects, or that you wanted to distribute for other developers to use in their projects, you had only a few not-very-good options:

  • Include your source code in every project that uses your library (increased compilation time, hooray!).
  • Build two library versions, one for armv6/armv7 and one for x86.
  • Screw with the linker to manually create a “fat” library.
  • Use the Bundle Hack and hope that it works.

It’s worth noting here that the first three options listed above do nothing to help with the management of any shared header files needed to actually make use of the library code. The fourth option does allow you to include public headers in your build artifact, but the setup to do so is tedious, and the entire thing is basically a giant hack besides. None of these options are really very convenient or useful from the developer’s point of view.

What developers really need, what they should have access to, and what XCode should have supported at least since the release of iOS 4.0, is the ability to build true universal iOS frameworks that are “real” frameworks in the same sense that UIKit and CoreGraphics are real frameworks. And that’s exactly what the iOS-Universal-Framework project gives you. Just install it and you get a handy XCode Project Template that can be used to build a universal iOS framework. This lets you build real iOS frameworks that include libraries for both x86 and armv6/armv7 in a single package, automatically bundled with your public header files. You (or anyone else) can then use the generated framework inside of other projects in the same way as any other iOS framework; just add it to your “Link With Binary Libraries” build phase, and away you go.

All told, the iOS-Universal-Framework Project Template is great and works exactly as advertised. Just install it and you’re good to go. I’ve only come across one minor caveat worth mentioning, which is that although the template does correctly produce a universal iOS framework, the build product shown in XCode will actually point to an architecture-specific version of the framework. In order to find the correct, universal framework it is necessary to do a “Show in Finder” on the build product shown in XCode (or otherwise navigate to your project output directory), and then go up a level (or two) in the filesystem to find the folder that contains the universal version of the framework (edit: as per Karl’s comment below, this has now been fixed. Hooray!).

Apart from that minor annoyance this little XCode extension is really quite excellent, and I encourage you to give it a try. My personal thanks to Karl Stenerud for putting this together.

Posted in coding, objective-c, process | Tagged , , | 1 Comment

[Objective-C + Cocoa] iPhone Screen Capture Revisited

Awhile back I posted a handful of simple iOS utilities. Among them was a basic ScreenCaptureView implementation that would periodically render the contents of its subview(s) into a UIImage that was exposed as a publicly accessible property. This provides the ability to quickly and easily take a snapshot of your running application, or any arbitrary component within it. And while not superbly impressive (the iPhone has a built-in screenshot feature, after all), I noted that the control theoretically allowed for captured frames to be sent off to an AVCaptureSession in order to record live video of a running application.

Recently I returned to this bit of code, and the ability to record live video of an application is theoretical no longer. To get straight to the point, here is the revised code:

//
//ScreenCaptureView.h
//
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

/**
 * Delegate protocol.  Implement this if you want to receive a notification when the
 * view completes a recording.
 *
 * When a recording is completed, the ScreenCaptureView will notify the delegate, passing
 * it the path to the created recording file if the recording was successful, or a value
 * of nil if the recording failed/could not be saved.
 */
@protocol ScreenCaptureViewDelegate <NSObject>
- (void) recordingFinished:(NSString*)outputPathOrNil;
@end


/**
 * ScreenCaptureView, a UIView subclass that periodically samples its current display
 * and stores it as a UIImage available through the 'currentScreen' property.  The
 * sample/update rate can be configured (within reason) by setting the 'frameRate'
 * property.
 *
 * This class can also be used to record real-time video of its subviews, using the
 * 'startRecording' and 'stopRecording' methods.  A new recording will overwrite any
 * previously made recording file, so if you want to create multiple recordings per
 * session (or across multiple sessions) then it is your responsibility to copy/back-up
 * the recording output file after each session.
 *
 * To use this class, you must link against the following frameworks:
 *
 *  - AssetsLibrary
 *  - AVFoundation
 *  - CoreGraphics
 *  - CoreMedia
 *  - CoreVideo
 *  - QuartzCore
 *
 */

@interface ScreenCaptureView : UIView {
   //video writing
   AVAssetWriter *videoWriter;
   AVAssetWriterInput *videoWriterInput;
   AVAssetWriterInputPixelBufferAdaptor *avAdaptor;

   //recording state
   BOOL _recording;
   NSDate* startedAt;
   void* bitmapData;
}

//for recording video
- (bool) startRecording;
- (void) stopRecording;

//for accessing the current screen and adjusting the capture rate, etc.
@property(retain) UIImage* currentScreen;
@property(assign) float frameRate;
@property(nonatomic, assign) id<ScreenCaptureViewDelegate> delegate;

@end



//
//ScreenCaptureView.m
//
#import "ScreenCaptureView.h"
#import <QuartzCore/QuartzCore.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <AssetsLibrary/AssetsLibrary.h>

@interface ScreenCaptureView(Private)
- (void) writeVideoFrameAtTime:(CMTime)time;
@end


@implementation ScreenCaptureView

@synthesize currentScreen, frameRate, delegate;

- (void) initialize {
   // Initialization code
   self.clearsContextBeforeDrawing = YES;
   self.currentScreen = nil;
   self.frameRate = 10.0f;     //10 frames per seconds
   _recording = false;
   videoWriter = nil;
   videoWriterInput = nil;
   avAdaptor = nil;
   startedAt = nil;
   bitmapData = NULL;
}

- (id) initWithCoder:(NSCoder *)aDecoder {
   self = [super initWithCoder:aDecoder];
   if (self) {
       [self initialize];
   }
   return self;
}

- (id) init {
   self = [super init];
   if (self) {
       [self initialize];
   }
   return self;
}

- (id)initWithFrame:(CGRect)frame {
   self = [super initWithFrame:frame];
   if (self) {
       [self initialize];
   }
   return self;
}

- (CGContextRef) createBitmapContextOfSize:(CGSize) size {
   CGContextRef    context = NULL;
   CGColorSpaceRef colorSpace;
   int             bitmapByteCount;
   int             bitmapBytesPerRow;

   bitmapBytesPerRow   = (size.width * 4);
   bitmapByteCount     = (bitmapBytesPerRow * size.height);
   colorSpace = CGColorSpaceCreateDeviceRGB();
   if (bitmapData != NULL) {
       free(bitmapData);
   }
   bitmapData = malloc( bitmapByteCount );
   if (bitmapData == NULL) {
       fprintf (stderr, "Memory not allocated!");
       return NULL;
   }

   context = CGBitmapContextCreate (bitmapData,
                                    size.width,
                                    size.height,
                                    8,      // bits per component
                                    bitmapBytesPerRow,
                                    colorSpace,
                                    kCGImageAlphaNoneSkipFirst);

   CGContextSetAllowsAntialiasing(context,NO);
   if (context== NULL) {
       free (bitmapData);
       fprintf (stderr, "Context not created!");
       return NULL;
   }
   CGColorSpaceRelease( colorSpace );

   return context;
}


//static int frameCount = 0;            //debugging
- (void) drawRect:(CGRect)rect {
   NSDate* start = [NSDate date];
   CGContextRef context = [self createBitmapContextOfSize:self.frame.size];
   
   //not sure why this is necessary...image renders upside-down and mirrored
   CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, self.frame.size.height);
   CGContextConcatCTM(context, flipVertical);

   [self.layer renderInContext:context];
   
   CGImageRef cgImage = CGBitmapContextCreateImage(context);
   UIImage* background = [UIImage imageWithCGImage: cgImage];
   CGImageRelease(cgImage);
   
   self.currentScreen = background;

   //debugging
   //if (frameCount < 40) {
   //      NSString* filename = [NSString stringWithFormat:@"Documents/frame_%d.png", frameCount];
   //      NSString* pngPath = [NSHomeDirectory() stringByAppendingPathComponent:filename];
   //      [UIImagePNGRepresentation(self.currentScreen) writeToFile: pngPath atomically: YES];
   //      frameCount++;
   //}

   //NOTE:  to record a scrollview while it is scrolling you need to implement your UIScrollViewDelegate such that it calls
   //       'setNeedsDisplay' on the ScreenCaptureView.
   if (_recording) {
       float millisElapsed = [[NSDate date] timeIntervalSinceDate:startedAt] * 1000.0;
       [self writeVideoFrameAtTime:CMTimeMake((int)millisElapsed, 1000)];
   }

   float processingSeconds = [[NSDate date] timeIntervalSinceDate:start];
   float delayRemaining = (1.0 / self.frameRate) - processingSeconds;

   CGContextRelease(context);

   //redraw at the specified framerate
   [self performSelector:@selector(setNeedsDisplay) withObject:nil afterDelay:delayRemaining > 0.0 ? delayRemaining : 0.01];   
}

- (void) cleanupWriter {
   [avAdaptor release];
   avAdaptor = nil;
   
   [videoWriterInput release];
   videoWriterInput = nil;
   
   [videoWriter release];
   videoWriter = nil;
   
   [startedAt release];
   startedAt = nil;

   if (bitmapData != NULL) {
       free(bitmapData);
       bitmapData = NULL;
   }
}

- (void)dealloc {
   [self cleanupWriter];
   [super dealloc];
}

- (NSURL*) tempFileURL {
   NSString* outputPath = [[NSString alloc] initWithFormat:@"%@/%@", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0], @"output.mp4"];
   NSURL* outputURL = [[NSURL alloc] initFileURLWithPath:outputPath];
   NSFileManager* fileManager = [NSFileManager defaultManager];
   if ([fileManager fileExistsAtPath:outputPath]) {
       NSError* error;
       if ([fileManager removeItemAtPath:outputPath error:&error] == NO) {
           NSLog(@"Could not delete old recording file at path:  %@", outputPath);
       }
   }

   [outputPath release];
   return [outputURL autorelease];
}

-(BOOL) setUpWriter {
   NSError* error = nil;
   videoWriter = [[AVAssetWriter alloc] initWithURL:[self tempFileURL] fileType:AVFileTypeQuickTimeMovie error:&error];
   NSParameterAssert(videoWriter);

   //Configure video
   NSDictionary* videoCompressionProps = [NSDictionary dictionaryWithObjectsAndKeys:
                                          [NSNumber numberWithDouble:1024.0*1024.0], AVVideoAverageBitRateKey,
                                          nil ];

   NSDictionary* videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                  AVVideoCodecH264, AVVideoCodecKey,
                                  [NSNumber numberWithInt:self.frame.size.width], AVVideoWidthKey,
                                  [NSNumber numberWithInt:self.frame.size.height], AVVideoHeightKey,
                                  videoCompressionProps, AVVideoCompressionPropertiesKey,
                                  nil];

   videoWriterInput = [[AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings] retain];

   NSParameterAssert(videoWriterInput);
   videoWriterInput.expectsMediaDataInRealTime = YES;
   NSDictionary* bufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys: 
                                     [NSNumber numberWithInt:kCVPixelFormatType_32ARGB], kCVPixelBufferPixelFormatTypeKey, nil];

   avAdaptor = [[AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:bufferAttributes] retain];

   //add input
   [videoWriter addInput:videoWriterInput];
   [videoWriter startWriting];
   [videoWriter startSessionAtSourceTime:CMTimeMake(0, 1000)];

   return YES;
}



- (void) completeRecordingSession {
   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

   [videoWriterInput markAsFinished];
   
   // Wait for the video
   int status = videoWriter.status;
   while (status == AVAssetWriterStatusUnknown) {
       NSLog(@"Waiting...");
       [NSThread sleepForTimeInterval:0.5f];
       status = videoWriter.status;
   }

   @synchronized(self) {
       BOOL success = [videoWriter finishWriting];
       if (!success) {
           NSLog(@"finishWriting returned NO");
       }

       [self cleanupWriter];

       id delegateObj = self.delegate;
       NSString *outputPath = [[NSString alloc] initWithFormat:@"%@/%@", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0], @"output.mp4"];
       NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:outputPath];

       NSLog(@"Completed recording, file is stored at:  %@", outputURL);
       if ([delegateObj respondsToSelector:@selector(recordingFinished:)]) {
           [delegateObj performSelectorOnMainThread:@selector(recordingFinished:) withObject:(success ? outputURL : nil) waitUntilDone:YES];
       }

       [outputPath release];
       [outputURL release];
   }

   [pool drain];
}



- (bool) startRecording {
   bool result = NO;
   @synchronized(self) {
       if (! _recording) {
           result = [self setUpWriter];
           startedAt = [[NSDate date] retain];
           _recording = true;
       }
   }

   return result;
}

- (void) stopRecording {
   @synchronized(self) {
       if (_recording) {
           _recording = false;
           [self completeRecordingSession];
       }
   }
}

-(void) writeVideoFrameAtTime:(CMTime)time {
   if (![videoWriterInput isReadyForMoreMediaData]) {
       NSLog(@"Not ready for video data");
   }
   else {
       @synchronized (self) {
           UIImage* newFrame = [self.currentScreen retain];
           CVPixelBufferRef pixelBuffer = NULL;
           CGImageRef cgImage = CGImageCreateCopy([newFrame CGImage]);
           CFDataRef image = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));

           int status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, avAdaptor.pixelBufferPool, &pixelBuffer);
           if(status != 0){
               //could not get a buffer from the pool
               NSLog(@"Error creating pixel buffer:  status=%d", status);
           }
                       // set image data into pixel buffer
           CVPixelBufferLockBaseAddress( pixelBuffer, 0 );
           uint8_t* destPixels = CVPixelBufferGetBaseAddress(pixelBuffer);
           CFDataGetBytes(image, CFRangeMake(0, CFDataGetLength(image)), destPixels);  //XXX:  will work if the pixel buffer is contiguous and has the same bytesPerRow as the input data

           if(status == 0){
               BOOL success = [avAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:time];
               if (!success)
                   NSLog(@"Warning:  Unable to write buffer to video");
           }

           //clean up
           [newFrame release];
           CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 );
           CVPixelBufferRelease( pixelBuffer );        
           CFRelease(image);
           CGImageRelease(cgImage);
       }

   }

}

@end

This class will let you record high-quality video of any other view in your application. To use it, simply set it up as the superview of the UIView(s) that you want to record, add a reference to it in your corresponding UIViewController (using Interface Builder or whatever your preferred method happens to be), and then call ‘startRecording‘ when you are ready to start recording video. When you’ve recorded enough, call ‘stopRecording‘ to complete the process. You will get a nice .mp4 file stored under your application’s ‘Documents’ directory that you can copy off or do whatever else you want with.

Note that if you want to record a UIScrollView while it is scrolling, you will need to implement your UIScrollViewDelegate such that it calls ‘setNeedsDisplay‘ on the ScreenCaptureView while the scroll-view is scrolling. For instance:

- (void) scrollViewDidScroll: (UIScrollView*)scrollView {
       [captureView setNeedsDisplay];
}

I haven’t tested this code on a physical device yet, but there’s no reason why it should not work on any device that includes H.264 video codec support (iPhone 3GS and later). However, given the amount of drawing that it does, it’s safe to say that the more horsepower behind it, the better.

Here is a rather unimpressive 30-second recording of a UITableView that I created using this class (if your browser doesn’t support HTML5, use the link below):

Example iPhone Recording

Lastly, I haven’t tested this class with any OpenGL-based subviews, so I can’t say if it will work in that case. If you try it in this configuration, please feel free to reply with your results.

Update

For anyone looking for a working example, you can download this sample project. This project simply creates a 30-second recording of a ‘UITableView‘.

Posted in coding, objective-c | Tagged , , , | 154 Comments

[Java] Quarantining Request Parameters

This is a follow-up on an earlier post in which I described a method for modifying HTTP request parameters on the fly in a servlet/web application. I mentioned that the technique could be used to provide a filter that automatically quarantines any potentially unsafe parameters so that application code/business logic does not need to worry about such things as XSS and SQL injection attacks, but I didn’t provide any complete reference code for such a filter. So today I aim to rectify that omission.

As this example code builds upon the OverridableHttpRequest class discussed in the previous post, you will need that class (or your own comparable implementation) before you get started. Once you have that, the code for the filter implementation is not very complex:

public class InputSanitizerFilter implements Filter {
	private static final Logger LOG = Logger.getLogger(InputSanitizerFilter.class);
	
	private static final String BANNED_INPUT_CHARS = ".*[^a-zA-Z0-9\\@\\'\\,\\.\\/\\(\\)\\+\\=\\-\\_\\[\\]\\{\\}\\^\\!\\*\\&\\%\\$\\:\\;\\? \\t]+.*";
	
	public static final String QUARANTINE_ATTRIBUTE_NAME = "filter.quarantined.params";
	public static final String SUSPICIOUS_REQUEST_FLAG_NAME = "filter.suspicious.request";

	@Override
	public void destroy() {
		//no work necessary
	}

	@Override
	@SuppressWarnings("unchecked")
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		//wrap the original request and set up an empty quarantine map
		OverridableHttpRequest newRequest = new OverridableHttpRequest((HttpServletRequest)request);
		Map<String, String> quarantine = new HashMap<String, String>();
		newRequest.setAttribute(QUARANTINE_ATTRIBUTE_NAME, quarantine);
		
		//inspect each parameter, and move any suspicious ones into thw quarantine area
		Enumeration<String> names = request.getParameterNames();
		while (names.hasMoreElements()) {
			String name = names.nextElement();
			String value = request.getParameter(name);
			if (value.matches(BANNED_INPUT_CHARS)) {
				//uh-oh, found something that doesn't look right, quarantine it and make sure the request is flagged as suspicious
				LOG.warn("Removing potentially malicious parameter from request:  " + name);
				quarantine.put(name, value);
				newRequest.removeParameter(name);
				newRequest.setAttribute(SUSPICIOUS_REQUEST_FLAG_NAME, "true");
			}
		}
		
		//done, send the modified request on down the chain
		chain.doFilter(newRequest, response);
	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		//no work necessary
	}
}

Basically there is a regex that defines the set of disallowed characters (anything that is not a letter, number, standard punctuation character, or whitespace…this example will also exclude any parameters that contain newlines as the webapp that it is used in does not contain any inputs that can accept newlines, but you can obviously modify it to your liking), and every parameter in the request is inspected to see if it contains one or more banned character(s). Any parameter that is found to include banned characters is copied into the ‘quarantine‘ map, and then removed as a parameter (so calling ‘request.getParameter(“badParam”)‘ will return null). This will prevent application code from ever seeing the suspect parameter(s), unless it explicitly goes looking inside of the quarantine map (which you might do if you want to allow users to have special characters in their password, for instance).

Also, if one or more parameters are quarantined, the request is flagged as suspicious by setting an attribute named ‘filter.suspicious.request‘. This gives code downstream from the filter a quick way to see if the request has been modified by the filter in any way, and allows the application to make its own decisions about whether or not it wants to venture into the quarantine area to inspect the potentially unsafe data. For instance:

if (request.getAttribute(InputSanitizerFilter.SUSPICIOUS_REQUEST_FLAG_NAME) != null) {
    Map<String, String> quarantine = (Map<String, String>)request.getAttribute(InputSanitizerFilter.QUARANTINE_ATTRIBUTE_NAME);
    for (String key : quarantine.keySet()) {
        System.out.println("Quarantined parameter:  name=" + key + ", value=" + quarantine.get(key));
    }
}

…or, with the right frameworks in place you might easily create something like an ‘@IgnoresQuarantine‘ annotation that can be applied selectively to business methods to mark a parameter (or set of parameters) as not subject to quarantine on calls to that method so that you never need to manually go looking through the quarantine area (after implementing the annotation and its backing logic). But that’s just slightly outside of the scope for today.

Posted in coding, java, servlet | Tagged , , | Leave a comment

[Java] URL Rewriting on Tomcat (or any other servlet container)

Here is a very nice little utility I found recently on an unfortunately very difficult to navigate website. In case it’s not immediately apparent from that site what I am referring to, I’m talking about the ‘UrlRewriteFilter‘ utility featured near the top of the page. This handy Filter implementation allows you to configure mod_rewrite style URL rewriting rules for your J2EE webapp. If you are having trouble navigating the official site, you can use this direct link to download ‘UrlRewriteFilter‘ version 3.2.

Sadly, apart from being difficult to navigate, some of the information on the official ‘UrlRewriteFilter‘ website pertaining to setup and usage of the filter is also incorrect/out of date. This is really quite a shame, because ‘UrlRewriteFilter‘ is an excellent little utility and I’m quite tired of seeing people needlessly running multi-server configurations (typically Apache httpd for static content, and something like Tomcat, Resin, Jetty, etc. for dynamic content) out of a desire to use this-or-that particular module that only works with httpd or (even worse) out of the outdated and no-longer-relevant notion that servlet containers cannot efficiently serve static content. So in an effort to save ‘UrlRewriteFilter‘ from obscurity and an undeserved death at the hands of poor documentation and a shoddy distribution site, here is a complete set of instructions for getting the filter to work in your webapp.

First, you will need to ensure that the ‘UrlRewriteFilter‘ JAR file is on your web-application’s classpath. How you do this will depend upon your build process/how you are constructing your webapp(s), but long story short placing the JAR file in your webapp under ‘/WEB-INF/lib’ will do the trick, and if you’ve spent any time at all working with webapps you probably already have a preferred way of doing this. Alternately, you may want to install the JAR file in your servlet container’s ‘/lib’ folder, particularly if you are deploying multiple webapps on your server and you want to have ‘UrlRewriteFilter‘ available to any/all of them automatically.

In any case, once you have the ‘UrlRewriteFilter‘ JAR on your webapp’s classpath, the real setup can begin. Open your application’s ‘web.xml‘ file, and add the following filter configuration to your webapp:

<!-- URL rewriting -->
<filter>
    	<filter-name>UrlRewriteFilter</filter-name>
      	<filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
      	<init-param>
        	<param-name>logLevel</param-name>
        	<param-value>WARN</param-value>
        </init-param>
</filter>
<filter-mapping>
    	<filter-name>UrlRewriteFilter</filter-name>
    	<url-pattern>/*</url-pattern>
</filter-mapping>

This instructs the servlet container to route every request that the server receives through the ‘UrlRewriteFilter‘. Note that although it is not discussed on the official site, that ‘logLevel‘ parameter is absolutely essential. If you omit it, the filter will fail to initialize properly and yield some very bizarre behavior.

Anyways, once your ‘web.xml‘ has been updated, the final step is to add a ‘urlrewrite.xml‘ file in the same directory as your ‘web.xml‘ file, and configure it to your liking. Here is an example ‘urlrewrite.xml‘ file with a couple basic rewrite rules:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.2//EN"
        "http://tuckey.org/res/dtds/urlrewrite3.2.dtd">
<urlrewrite>
    <!-- user-account activation link -->
    <rule>
    	<from>/activate/([a-f0-9]+)?(.*)</from>
     	<to>/userController?action=activateUser&amp;token=$1&amp;$2</to>
    </rule>

    <!-- default rules included with urlrewrite -->
    <rule>
        <note>
            The rule means that requests to /test/status/ will be redirected to /rewrite-status
            the url will be rewritten.
        </note>
        <from>/test/status/</from>
        <to type="redirect">%{context-path}/rewrite-status</to>
    </rule>
    <outbound-rule>
        <note>
            The outbound-rule specifies that when response.encodeURL is called (if you are using JSTL c:url)
            the url /rewrite-status will be rewritten to /test/status/.

            The above rule and this outbound-rule means that end users should never see the
            url /rewrite-status only /test/status/ both in thier location bar and in hyperlinks
            in your pages.
        </note>
        <from>/rewrite-status</from>
        <to>/test/status/</to>
    </outbound-rule> 
</urlrewrite>

This defines two rules. The first simply rewrites URL’s of the form ‘/activate/######?[params]‘ to something like ‘/userController?action=activateUser&token=######&[params]‘, and the second is the default example rule that comes with ‘UrlRewriteFilter‘ and allows you to see a basic diagnostic page by pointing your browser at ‘[your server]/test/status‘.

And there you have it. Quite simple, really, and now there’s one less reason to continue running two distinct server instances where one would do just as well.

Posted in coding, configuration, java, servlet, software | Tagged , , | 3 Comments

[Status] The Witcher 2

For anyone that missed it, the sequel to one of the best games ever put together was released a few days ago. I’ve been busy quickly re-playing the first game so that I could have a save-game to import into the second. And with that now complete, it’s finally time to start playing The Witcher 2.

So long story short, I expect it to be fairly quiet around here, until I finish with this latest distraction.

Posted in status | Leave a comment

[Java] Override HTTP Request Parameters

Often in a Java web-application I come across cases where it would be useful to directly override or modify one or more HTTP request parameters. To be clear, by “request parameter” I am referring to the value that is returned by the ServletRequest‘s ‘getParameter()‘ method. For whatever reason the architects of the Servlet spec decided that direct modification of request parameters was not to be supported, despite the number of use-cases that can benefit from such a feature.

For example, say that you have a Filter that you are using to sanitize input parameters and guard against things like XSS attacks by ensuring that obviously invalid values like “<script>alert(‘hacked!’);</script>” are filtered out. Wouldn’t it be great if you could implement your Filter such that when it finds one or more forbidden values it flags the request as potentially malicious (using a request attribute), removes any parameters that contain potentially unsafe data, and relocates the potentially unsafe data to a predetermined quarantine area (also accessible via a request attribute)? This would protect your webapp code from ever receiving a malicious parameter, while still allowing parts of the code that might permit seemingly malicious parameters (for instance, there is no reason to prevent a user from registering with a password of “<script>alert(‘hacked!’);</script>” if that is what they want to use, particularly if you are hashing user passwords like you should be) to still access them if desired by going through the quarantine area.

Of course, two-thirds of the functionality described above can be implemented without being able to override request parameters. With the standard API you can certainly check for potentially malicious parameters, set an attribute if you find any, and copy their values into a quarantine area. But what you cannot do is remove the parameters from the request, so any application code that directly accesses a request parameter value may still be at risk, particularly if its author forgets to check to see if the request has been flagged as suspect. The real beauty of being able to override parameter values is that you can do things like completely prevent a malicious parameter from ever being visible to your application code unless your application code goes out of its way to look for it (and if you do that, and you do it incorrectly, then that’s your own fault).

Anyways, the code to enable this kind of functionality is a fairly straightforward (if tedious) exercise in writing an HttpServletRequest wrapper and then overriding a few choice methods (and adding a couple new ones):

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

public class OverridableHttpRequest implements HttpServletRequest {
        
	private HttpServletRequest wrappedRequest;
	private Map<String, String> newParams;
	private Set<String> removedParams;

	public OverridableHttpRequest(HttpServletRequest requestToWrap) {
		this.wrappedRequest = requestToWrap;
		this.newParams = new HashMap<String, String>();
		this.removedParams = new HashSet<String>();
	}

	// these things we add so that params can be overridden
	public void setParameter(String name, String value) {
		this.removedParams.remove(name);
		this.newParams.put(name, value);
	}

	public void removeParameter(String name) {
		this.newParams.remove(name);
		this.removedParams.add(name);
	}

	// these things we need to override so that the correct state is exposed through the standard API
	@SuppressWarnings("rawtypes")
	@Override
	public Enumeration getParameterNames() {
		Set<String> result = new HashSet<String>();
		Enumeration requestParams = this.wrappedRequest.getParameterNames();
		while (requestParams.hasMoreElements()) {
			Object param = requestParams.nextElement();
			if (!removedParams.contains(param)) {
				result.add((String) param);
			}
		}
		result.addAll(newParams.keySet());

		return Collections.enumeration(result);
	}

	@Override
	public String[] getParameterValues(String arg0) {
		//NOTE:  not strictly to spec
		String[] result = new String[1];
		result[0] = this.getParameter(arg0);

		return result;
	}

	@Override
	public String getParameter(String arg0) {
		if (removedParams.contains(arg0)) {
			return null;
		}
		if (newParams.containsKey(arg0)) {
			return newParams.get(arg0);
		}
		return this.wrappedRequest.getParameter(arg0);
	}

	@SuppressWarnings("rawtypes")
	@Override
	public Map getParameterMap() {
		Map<String, String[]> result = new HashMap<String, String[]>();
		for (Object key : this.wrappedRequest.getParameterMap().keySet()) {
			result.put((String)key, (String[])this.wrappedRequest.getParameterMap().get(key));
		}
		for (String key : this.newParams.keySet()) {
			result.put(key, new String[] {this.newParams.get(key)});
		}
		for (String key : this.removedParams) {
			result.remove(key);
		}
		
		return result;
	}

	// these things we should probably override but don't right now
	@Override
	public String getRequestURI() {
		// FIXME: should return a modified URI based upon current state
		return this.wrappedRequest.getRequestURI();
	}

	@Override
	public StringBuffer getRequestURL() {
		// FIXME: should return a modified URL based upon current state
		return this.wrappedRequest.getRequestURL();
	}

	@Override
	public String getQueryString() {
		// FIXME: should return a modified String based upon current state
		return this.wrappedRequest.getQueryString();
	}

	// everything else just passes through
	@Override
	public Object getAttribute(String arg0) {
		return this.wrappedRequest.getAttribute(arg0);
	}

	@SuppressWarnings("rawtypes")
	@Override
	public Enumeration getAttributeNames() {
		return this.wrappedRequest.getAttributeNames();
	}

	@Override
	public String getCharacterEncoding() {
		return this.wrappedRequest.getCharacterEncoding();
	}

	@Override
	public int getContentLength() {
		return this.wrappedRequest.getContentLength();
	}

	@Override
	public String getContentType() {
		return this.wrappedRequest.getContentType();
	}

	@Override
	public ServletInputStream getInputStream() throws IOException {
		return this.wrappedRequest.getInputStream();
	}

	@Override
	public String getLocalAddr() {
		return this.wrappedRequest.getLocalAddr();
	}

	@Override
	public String getLocalName() {
		return this.wrappedRequest.getLocalName();
	}

	@Override
	public int getLocalPort() {
		return this.wrappedRequest.getLocalPort();
	}

	@Override
	public Locale getLocale() {
		return this.wrappedRequest.getLocale();
	}

	@SuppressWarnings("rawtypes")
	@Override
	public Enumeration getLocales() {
		return this.wrappedRequest.getLocales();
	}

	@Override
	public String getProtocol() {
		return this.wrappedRequest.getProtocol();
	}

	@Override
	public BufferedReader getReader() throws IOException {
		return this.wrappedRequest.getReader();
	}

	@SuppressWarnings("deprecation")
	@Override
	public String getRealPath(String arg0) {
		return this.wrappedRequest.getRealPath(arg0);
	}

	@Override
	public String getRemoteAddr() {
		return this.wrappedRequest.getRemoteAddr();
	}

	@Override
	public String getRemoteHost() {
		return this.wrappedRequest.getRemoteHost();
	}

	@Override
	public int getRemotePort() {
		return this.wrappedRequest.getRemotePort();
	}

	@Override
	public RequestDispatcher getRequestDispatcher(String arg0) {
		return this.wrappedRequest.getRequestDispatcher(arg0);
	}

	@Override
	public String getScheme() {
		return this.wrappedRequest.getScheme();
	}

	@Override
	public String getServerName() {
		return this.wrappedRequest.getServerName();
	}

	@Override
	public int getServerPort() {
		return this.wrappedRequest.getServerPort();
	}

	@Override
	public boolean isSecure() {
		return this.wrappedRequest.isSecure();
	}

	@Override
	public void removeAttribute(String arg0) {
		this.wrappedRequest.removeAttribute(arg0);
	}

	@Override
	public void setAttribute(String arg0, Object arg1) {
		this.wrappedRequest.setAttribute(arg0, arg1);
	}

	@Override
	public void setCharacterEncoding(String arg0)
			throws UnsupportedEncodingException {
		this.wrappedRequest.setCharacterEncoding(arg0);
	}

	@Override
	public String getAuthType() {
		return this.wrappedRequest.getAuthType();
	}

	@Override
	public String getContextPath() {
		return this.wrappedRequest.getContextPath();
	}

	@Override
	public Cookie[] getCookies() {
		return this.wrappedRequest.getCookies();
	}

	@Override
	public long getDateHeader(String arg0) {
		return this.wrappedRequest.getDateHeader(arg0);
	}

	@Override
	public String getHeader(String arg0) {
		return this.wrappedRequest.getHeader(arg0);
	}

	@SuppressWarnings("rawtypes")
	@Override
	public Enumeration getHeaderNames() {
		return this.wrappedRequest.getHeaderNames();
	}

	@SuppressWarnings("rawtypes")
	@Override
	public Enumeration getHeaders(String arg0) {
		return this.wrappedRequest.getHeaders(arg0);
	}

	@Override
	public int getIntHeader(String arg0) {
		return this.wrappedRequest.getIntHeader(arg0);
	}

	@Override
	public String getMethod() {
		return this.wrappedRequest.getMethod();
	}

	@Override
	public String getPathInfo() {
		return this.wrappedRequest.getPathInfo();
	}

	@Override
	public String getPathTranslated() {
		return this.wrappedRequest.getPathTranslated();
	}

	@Override
	public String getRemoteUser() {
		return this.wrappedRequest.getRemoteUser();
	}

	@Override
	public String getRequestedSessionId() {
		return this.wrappedRequest.getRequestedSessionId();
	}

	@Override
	public String getServletPath() {
		return this.wrappedRequest.getServletPath();
	}

	@Override
	public HttpSession getSession() {
		return this.wrappedRequest.getSession();
	}

	@Override
	public HttpSession getSession(boolean arg0) {
		return this.wrappedRequest.getSession(arg0);
	}

	@Override
	public Principal getUserPrincipal() {
		return this.wrappedRequest.getUserPrincipal();
	}

	@Override
	public boolean isRequestedSessionIdFromCookie() {
		return this.wrappedRequest.isRequestedSessionIdFromCookie();
	}

	@Override
	public boolean isRequestedSessionIdFromURL() {
		return this.wrappedRequest.isRequestedSessionIdFromURL();
	}

	@SuppressWarnings("deprecation")
	@Override
	public boolean isRequestedSessionIdFromUrl() {
		return this.wrappedRequest.isRequestedSessionIdFromUrl();
	}

	@Override
	public boolean isRequestedSessionIdValid() {
		return this.wrappedRequest.isRequestedSessionIdValid();
	}

	@Override
	public boolean isUserInRole(String arg0) {
		return this.wrappedRequest.isUserInRole(arg0);
	}

}

So the new methods being added here are ‘removeParameter(String name)‘ and ‘setParameter(String name)‘, which do pretty much what their name implies. If you are familiar with the standard ‘removeAttribute(String name)‘ and ‘setAttribute(String name)‘ methods, then you should feel right at home with these new additions. They simply let you manipulate request parameters in a way that’s identical to how you can already manipulate request attributes.

One minor deviation from the Servlet specification that is worth noting is that I have overridden ‘getParameterValues(String name)‘ such that it only returns the first value associated with a given parameter. This means that if for some reason your webapp uses URL’s like “http://mysite.com/api?user=bob&user=jane&user=paul” then you will only see “bob” as a value for the ‘user’ parameter. In practice I have not ever come across a web application that intentionally relied on a single parameter name having multiple values associated with it, and if you are designing your web application in such a way then you should probably just stop and pick a less confusing pattern. I see no value in a feature that allows a single parameter to have multiple values that you only get to see if you use a different API method to get them (‘getParameterValues‘ instead of ‘getParameter‘), and so I have removed this feature from the implementation. If someone can come up with a solid justification for having such a feature, I will add it back in.

Also, left as an exercise is overriding ‘getRequestURI()‘, ‘getRequestURL()‘, and ‘getQueryString()‘ to return the correct values based upon the modified request state. It’s fairly rare to have application code that depends upon the values of these calls, so in most cases you will not need to do this.

In any case, to make use of the OverridableHttpRequest class, you can do the following:

public class ExampleFilter implements Filter {
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		//do initialization things here
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain filterChain) throws IOException, ServletException {

		HttpServletRequest httpReq = (HttpServletRequest) response;
		OverridableHttpRequest newRequest = new OverridableHttpRequest(httpReq);
     
		//do work and modify the request as desired
		newRequest.removeParameter("someBadXssParam");

		//pass the modified request on to the webapp, anyone downstream will see 
		//the modified state with no 'someBadXssParam' in it
		filterChain.doFilter(newRequest, response);
	}

	@Override
	public void destroy() {
		//do shutdown things here
	}
}

Simple, but powerful. I just wish the Servlet spec included this kind of functionality out of the box so that it wouldn’t be necessary to implement a complete HttpServletRequest wrapper just to add a couple of basic mutator methods.

Posted in coding, java | Tagged , , | 1 Comment

Social Reputation; A Tale of Two Websites

I was recently introduced to Mixtent, a social website with the goal of assigning a reliable professional reputation to its users across a number of distinct skills. For more background information, here is a TechCrunch article from Mixtent’s launch in late January. In essence they hope to build an accurate and reliable database of peoples’ professional abilities, presumably so that it might prove a useful tool for recruitment agencies and HR departments alike (whom they can charge for access, of course). And while I think this is an interesting and worthwhile goal, I think Mixtent is going about it in completely the wrong way, as can be demonstrated by contrasting Mixtent with another website which accomplishes (though perhaps inadvertently) the same thing through very different means; StackOverflow.

Mixtent is relatively new to the scene, but it follows an all-too-familiar structure. First, you cannot do anything until you are connected to a sufficient number of people on Mixtent. Don’t have enough connections? Then never fear, Mixtent will kindly offer to trawl through your Gmail, Facebook and LinkedIn accounts and bug all of your friends and contacts to come and join you on Mixtent (and to all my fellow developers, can we please stop using this obnoxious pattern? The goal should be to build something that is so cool that people want to show it to their friends, not to build something that tries to make itself look cool by coercing people into showing it to their friends. If you need to force people to share your creation with their friends, then you have already failed). Once you have amassed a large enough number of contacts to appease the Mixtent gods, you get to see what the application is all about. Basically, you give your opinion about how well your contacts stack up in various areas of expertise, and they do the same for you (and of course, as a “social” app, your visibility and interactions are limited to just your immediate circle of contacts). Simple enough, really, but is there any value in it?

At its most fundamental level and like so many other social offerings, Mixtent is nothing more than a glorified popularity contest, and this makes any sort of reputation score that it might generate highly suspect. If you’re popular enough to have a large network and are well-liked by the people in that network, then you are almost guaranteed to have a very good reputation on Mixtent. But does that reputation mean that you are actually skilled professionally, or does it just mean that you are popular and well-liked? At a minimum one can say that it means that you may be skilled professionally, but since Mixtent ratings are just the composite of the personal opinions of your social contacts and are not backed up by anything substantial, they certainly cannot be taken to mean that you are definitely skilled in any of your claimed areas of proficiency. From a practical/hiring standpoint, they ought to be considered all but worthless.

In fact, I’m pretty sure I remember a Facebook app that did more or less the same thing as Mixtent (albeit with even less relevant topics like “who would you rather hook up with”), and I can’t imagine anyone using the output of such an application to make hiring decisions. At its core, the output of this Facebook app was little more than amplified conjecture, opinion, and hearsay, and sadly such is the case with Mixtent as well.

So let’s contrast the Mixtent experience with the StackOverflow experience. One major difference that’s readily apparent is that while the StackOverflow experience is far more socially engaging than the Mixtent experience, StackOverflow has no underlying social architecture to speak of. You cannot have friends, contacts, acquaintances, or any other direct relationship between your account and another user’s. The structure has more in common with an online forum than a social-networking website. This has at least one major positive impact; on StackOverflow, there is no nagging to invite all your friends, and no limitation of functionality until you have built some minimal number of connections (although you do gain access to additional privileges as you build a better reputation, none of these privileges are essential to the use of the site; all the basic functionality is open to everyone). Anyone can sign up for an account (or just log in with an existing, supported OpenID account) and get started instantly.

Another major difference is that on StackOverflow, people don’t rate you directly. Instead they rate your contributions to the community, and your overall reputation is determined by the overall value of all your contributions. This means that building a positive reputation takes much more work than it does on Mixtent. No longer is it sufficient to be popular and well-liked. Instead you must devote time to the community, make contributions, and ensure that your contributions are good enough that other community members will acknowledge their value. So there is no fast-track to the top of StackOverflow. This may be seen by some as a negative, but the trade-off is that unlike Mixtent rankings, a StackOverflow reputation score actually means something. If a user has a high positive reputation, it means that they worked hard to earn it by making contributions that their peers deemed valuable. Their work has been repeatedly marked as useful by other community members, so it’s safe to say that they probably know what they’re talking about.

The core difference between these two approaches, then, is that in the Mixtent version your peers modify your reputation by rating you directly as a person (your reputation is a function of how well other people like you), while in the StackOverflow version your peers modify your reputation by rating you indirectly through your contributions (your reputation is a function of how well other people like the work that you produce). One approach allows you to quickly compute a reputation that is based more upon popularity than anything else, while the other takes significantly longer but produces a reputation that accurately reflects the value of a person’s work in a given area. I think it’s clear which one of these I think is more valuable, and if I were a hiring manager, I would certainly prefer seeing someone’s StackOverflow profile over their Mixtent reputation any day of the week.

The interesting thing is that StackOverflow has managed to be better at Mixtent than Mixtent, even though it was never intended that way, and even though it lacks any sort of social framework. All the same basic features are there. For instance, there are multiple networks for different distinct topics (coding, law, mechanics, etc.), and reputation does not carry over from one network to another, and as you contribute and gain reputation in a topic all of your contributions are tagged with the specific area of expertise they apply to, and these tags become an integral part of your profile/overall reputation, saying in which specific areas your core strengths lie.

That said, I do think there is room in the equation for a social infrastructure. I can imagine something similar to StackOverflow, but with private or semi-private groups that can be used to create associations between users. These groups might be specific to a company (like “all employees at Google”) and/or perhaps to a specific problem-domain or area of interest. Then you can retain all the existing public interactions, but also add on top of it an additional layer of interaction that happens with one’s networked peers. A person’s contribution within their social groups might be accessible only to other members of that same group but still factored in as a component of their overall reputation. Such a setup would also allow for interesting queries, such as “who is the highest-ranking contributor at Google” and “which company has the highest average employee reputation”, among others.

So in the end, I think Mixtent gets a lot of things wrong, StackOverflow gets a lot of things right, and there’s still room for improvement in the domain of efficiently computing a reliable, meaningful social reputation score. I would love to see someone take up the challenge of either building a StackOverflow-like site on top of a social architecture, or of patching a social network into StackOverflow. I think the end result of such a project would be something awesome.

Posted in editorial | Tagged , , | Leave a comment

Webcomix 1.0!

It has been a little less than 1.5 months since the Webcomix beta was announced, and I think the project has finally matured to the point where it need no longer be considered a beta. During this time the number of supported comics was more than doubled, the comic-selection UI has been completely reworked, features that weren’t working quite right at the start of the beta have been fixed, and a number of new features have been added, such as live thumbnails, Facebook integration, and more.

Most importantly, the original architecture and caching hierarchy have been validated as capable of efficiently managing requests for a large number of comics pulled from several distinct data-sources, and the API verified as sufficient for creating complex comic-centric applications. Overall I am fairly pleased with the current state of the project, and thus I give you:

Webcomix 1.0

Webcomix 1.0

Of course, the non-beta status does not mean that development on the project is stopping, or that Webcomix is free of bugs and issues. Neither of those is true, and you should expect to see periodic updates taking place to roll out new content and features and to fix and/or improve existing features and behavior. Next up is twitter integration and a simple caching improvement to reduce the amount of time it takes to determine what links should display below a comic. Then perhaps a re-examining of how the Facebook integration is currently working and some fixes for some minor/difficult to trigger bugs, followed by a steady roll-out of new content and features.

Anyways, if you missed it before then I invite you to take a look at Webcomix. All you have to do is sit back, read some comics, and enjoy.

Posted in banter | 4 Comments

[Status] Anti-spam Enabled

The volume of inbound comment spam has suddenly increased from “annoying” to “completely ridiculous” and exceeded my capacity to deal with it manually. So I’ve done what I probably should have done in the first place and enabled the Akismet anti-spam plugin that came bundled with this WordPress distribution. It seems to be working, but I haven’t done much checking to verify that it isn’t also filtering out legitimate comments with the spam.

If you find your comments being erroneously blocked, please contact me directly via e-mail at ‘aroth [dot] bigtribe [at] gmail [dot] com’ and I will see what I can do to tweak the spam filter settings.

Posted in status | Leave a comment

Portal 2

For anyone that hasn’t noticed, Portal 2 was released a few days ago. I’ve already played through the single-player portion of the game (the only portion that I’m interested in, to be honest), and thought I would write up a review.

Long story short, my feelings toward Portal 2 are a bit mixed. Viewed as a game in its own right, Portal 2 does not disappoint. It’s really hard to find much to fault it for in this context. But viewed as a continuation of the original Portal, the game just doesn’t quite live up to the sky-high expectations that I (and probably other fans of the original game) had for it. GLaDOS is back, and just as sarcastic, humorous, and passive-aggressive as ever. At least for the first half of the game. Something seems to happen to the writing about halfway through; it loses its clever wit, that special something-or-other that left you anticipating the next catty remark from GLaDOS and laughing-out-loud when it finally arrived.

Much of the dialog towards the end of the game becomes just matter-of-fact moving-the-story-along chatter, with one AI acting loopy and another AI trying to provide a plausible explanation for why the first one is acting loopy. After a certain point, it just doesn’t feel as clever. At first I thought that it might be just because your antagonist changes, but that’s not really it. Wheatley’s early game dialog is on par with anything GLaDOS ever said, but as the game progresses his lines become less and less inspired. By the time you reach the end some of them are so bland that you pretty much have to force yourself to listen.

In the end, I think the developers erred in putting too much effort into trying to explain and justify their game world. Why should I care about history when there is a sarcastic, witty, and antagonistic AI practically goading me into dismantling it? Such was the case in Portal, but unfortunately not so in Portal 2. Instead the game distracts itself in trying to explain the origins of its world and the players within it, and the result is that some of the drive to keep pushing forward is lost.

Moving along, Portal had its fair share of fiendishly difficult levels. Levels which would leave you genuinely stumped and probably kill you once or twice before you puzzled your way out of them. Portal 2 has a couple of moderately challenging levels, but for the most part the puzzles are straightforward and easy to solve. I’m glad that for the most part the developers fixed it so that going through a portal won’t arbitrarily rotate your character’s orientation (and/or they made it easier to avoid placing portals such that they force you to rotate), but apart from that I think the game was nerfed a bit too much.

Portal put you on a conveyor belt heading towards an incinerator and left you with very little time to work out an escape plan. The situation felt very tense the first time through, and if you did not react very quickly to locate the way out then the game had no qualms about killing you for your shortcoming. Portal 2 has a similar scenario, but the way out of it is much too obvious and the element of danger just isn’t there. Rather than being compelled to react, it feels like there’s plenty of time to just stand their and listen to the AI gloat about its triumph. The sense of imminent danger just isn’t there like it was in the original game.

That said, there’s very little to complain about in terms of graphics, sound or gameplay. Any reviewer who gripes about Portal 2 (PC version, obviously) suffering from being a “port of the console version” is just being a fool. Portal 2 was implemented using the Source engine, an engine which began its life first and foremost as a PC game engine. It cannot be a “port” of anything if its implementation engine natively supports the PC as a target platform.

Yes the graphics in Portal 2 are a bit simple/dated-looking, but such has been the case with every Source-engine game released (even the very first games to use the engine). Valve has always favored attaining playable framerates on lower-end hardware over throwing in lots of hardware-crushing eye-candy, and I’m not about to fault them for their decision. If you need hardware-crushing eye-candy, play Crysis.

On top of its solid graphics and gameplay, Portal 2 gives you quite a few new puzzles to play through. In fact, there are almost certainly more puzzles in the Portal 2 single-player game than in Portal’s single-player game. But, because of the decreased level of difficulty, most of these new puzzles can be breezed through in a handful of minutes, even on your first play-through. It took me less then 7 hours to complete the single-player game, and I wasn’t rushing or trying to solve puzzles as quickly as possible.

I spent time exploring the environment and looking around, trying to knock off as many achievements as I could, and still I only needed 7 hours to complete the game. That’s a bit short, but the problem isn’t that the game itself is too short, rather it’s that the level of difficulty has been reduced too much. Once you know the solutions, Portal can be completed in far less than 7 hours. Portal 2’s real problem is that too many of the solutions are a bit too obvious.

Portal 2 does introduce a number of new gameplay elements, from the propulsion/repulsion/conversion gels, to mirrored “discouragement redirection cubes”, to hard-light bridges, excursion beams, and “aerial faith-plates”. The last of these I find a bit questionable, as their real purpose seems to be replacing some of Portal’s more challenging jumps (the ones where you have to work out some way to build sufficient momentum to bridge an impassable chasm of some sort or another) with a prepackaged solution that “just works”.

Unlike the gels, mirrors, bridges, and beams, the aerial faith-plates function less as a tool for solving the current puzzle and more like a quick way of hopping between Point A and Point B without using a portal and without having to worry about where you land. I think the game could have been better without them, but apart from that I also think that the rest of the new gameplay elements function very well within the context of the game, and I’m surprised that Valve was able to come up with so many new physics things to add to the game.

One last thing that leaves me a little confused is the decision to replace the guided missiles of Portal with generic unpropelled exploding boxes in Portal 2. It doesn’t really change the gameplay that much, but personally I found the missiles to be a lot more threatening and fun than some nondescript red boxes that just happen to explode when they hit something. Perhaps Valve decided that they didn’t want to renew their missile targeting AI license?

Anyways, though I have picked the game apart over a few minor issues, I still think that overall Portal 2 is a very fun game and worth a play-through or two. Play it and enjoy it as a game in its own right, as its biggest flaws only become apparent when you hold it to the candle that is Portal the first. It doesn’t quite satisfy as a sequel to that original magical title, but as a first-person puzzle game with a completely unique mode of gameplay there’s still nothing else quite like it.

Overall score: 8.5 / 10.0

P.S. The Portal Song is an order of magnitude better than The Portal 2 Song. So don’t get your hopes up expecting an epic musical ending like in the first game. You still get a musical ending, but it’s not quite as epic.

Posted in gaming, software | Tagged , , | Leave a comment