/**
  * Serve a request for a minified file. 
  * 
  * Here are the available options and defaults in the base controller:
  * 
  * 'isPublic' : send "public" instead of "private" in Cache-Control 
  * headers, allowing shared caches to cache the output. (default true)
  * 
  * 'quiet' : set to true to have serve() return an array rather than sending
  * any headers/output (default false)
  * 
  * 'encodeOutput' : set to false to disable content encoding, and not send
  * the Vary header (default true)
  * 
  * 'encodeMethod' : generally you should let this be determined by 
  * HTTP_Encoder (leave null), but you can force a particular encoding
  * to be returned, by setting this to 'gzip' or '' (no encoding)
  * 
  * 'encodeLevel' : level of encoding compression (0 to 9, default 9)
  * 
  * 'contentTypeCharset' : appended to the Content-Type header sent. Set to a falsey
  * value to remove. (default 'utf-8')  
  * 
  * 'maxAge' : set this to the number of seconds the client should use its cache
  * before revalidating with the server. This sets Cache-Control: max-age and the
  * Expires header. Unlike the old 'setExpires' setting, this setting will NOT
  * prevent conditional GETs. Note this has nothing to do with server-side caching.
  * 
  * 'rewriteCssUris' : If true, serve() will automatically set the 'currentDir'
  * minifier option to enable URI rewriting in CSS files (default true)
  * 
  * 'bubbleCssImports' : If true, all @import declarations in combined CSS
  * files will be move to the top. Note this may alter effective CSS values
  * due to a change in order. (default false)
  * 
  * 'debug' : set to true to minify all sources with the 'Lines' controller, which
  * eases the debugging of combined files. This also prevents 304 responses.
  * @see Minify_Lines::minify()
  * 
  * 'minifiers' : to override Minify's default choice of minifier function for 
  * a particular content-type, specify your callback under the key of the 
  * content-type:
  * <code>
  * // call customCssMinifier($css) for all CSS minification
  * $options['minifiers'][Minify::TYPE_CSS] = 'customCssMinifier';
  * 
  * // don't minify Javascript at all
  * $options['minifiers'][Minify::TYPE_JS] = '';
  * </code>
  * 
  * 'minifierOptions' : to send options to the minifier function, specify your options
  * under the key of the content-type. E.g. To send the CSS minifier an option: 
  * <code>
  * // give CSS minifier array('optionName' => 'optionValue') as 2nd argument 
  * $options['minifierOptions'][Minify::TYPE_CSS]['optionName'] = 'optionValue';
  * </code>
  * 
  * 'contentType' : (optional) this is only needed if your file extension is not 
  * js/css/html. The given content-type will be sent regardless of source file
  * extension, so this should not be used in a Groups config with other
  * Javascript/CSS files.
  * 
  * Any controller options are documented in that controller's setupSources() method.
  * 
  * @param mixed instance of subclass of Minify_Controller_Base or string name of
  * controller. E.g. 'Files'
  * 
  * @param array $options controller/serve options
  * 
  * @return mixed null, or, if the 'quiet' option is set to true, an array
  * with keys "success" (bool), "statusCode" (int), "content" (string), and
  * "headers" (array).
  */
 public static function serve($controller, $options = array())
 {
     if (is_string($controller)) {
         // make $controller into object
         $class = 'Diglin_Minify_Controller_' . $controller;
         if (!class_exists($class, false)) {
             #require_once "Diglin/Minify/Controller/" . str_replace('_', '/', $controller) . ".php";
         }
         $controller = new $class();
     }
     // set up controller sources and mix remaining options with
     // controller defaults
     $options = $controller->setupSources($options);
     $options = $controller->analyzeSources($options);
     self::$_options = $controller->mixInDefaultOptions($options);
     // check request validity
     if (!$controller->sources) {
         // invalid request!
         if (!self::$_options['quiet']) {
             header(self::$_options['badRequestHeader']);
             echo self::$_options['badRequestHeader'];
             return;
         } else {
             list(, $statusCode) = explode(' ', self::$_options['badRequestHeader']);
             return array('success' => false, 'statusCode' => (int) $statusCode, 'content' => '', 'headers' => array());
         }
     }
     self::$_controller = $controller;
     if (self::$_options['debug']) {
         self::_setupDebug($controller->sources);
         self::$_options['maxAge'] = 0;
     }
     // determine encoding
     if (self::$_options['encodeOutput']) {
         if (self::$_options['encodeMethod'] !== null) {
             // controller specifically requested this
             $contentEncoding = self::$_options['encodeMethod'];
         } else {
             // sniff request header
             #require_once 'HTTP/Encoder.php';
             // depending on what the client accepts, $contentEncoding may be
             // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling
             // getAcceptedEncoding(false, false) leaves out compress and deflate as options.
             list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false);
         }
     } else {
         self::$_options['encodeMethod'] = '';
         // identity (no encoding)
     }
     // check client cache
     #require_once 'HTTP/ConditionalGet.php';
     $cgOptions = array('lastModifiedTime' => self::$_options['lastModifiedTime'], 'isPublic' => self::$_options['isPublic'], 'encoding' => self::$_options['encodeMethod']);
     if (self::$_options['maxAge'] > 0) {
         $cgOptions['maxAge'] = self::$_options['maxAge'];
     }
     $cg = new Diglin_HTTP_ConditionalGet($cgOptions);
     if ($cg->cacheIsValid) {
         // client's cache is valid
         if (!self::$_options['quiet']) {
             $cg->sendHeaders();
             return;
         } else {
             return array('success' => true, 'statusCode' => 304, 'content' => '', 'headers' => $cg->getHeaders());
         }
     } else {
         // client will need output
         $headers = $cg->getHeaders();
         unset($cg);
     }
     if (self::$_options['contentType'] === self::TYPE_CSS && self::$_options['rewriteCssUris']) {
         reset($controller->sources);
         while (list($key, $source) = each($controller->sources)) {
             if ($source->filepath && !isset($source->minifyOptions['currentDir']) && !isset($source->minifyOptions['prependRelativePath'])) {
                 $source->minifyOptions['currentDir'] = dirname($source->filepath);
             }
         }
     }
     // check server cache
     if (null !== self::$_cache) {
         // using cache
         // the goal is to use only the cache methods to sniff the length and
         // output the content, as they do not require ever loading the file into
         // memory.
         $cacheId = 'minify_' . self::_getCacheId();
         $fullCacheId = self::$_options['encodeMethod'] ? $cacheId . '.gz' : $cacheId;
         // check cache for valid entry
         $cacheIsReady = self::$_cache->isValid($fullCacheId, self::$_options['lastModifiedTime']);
         if ($cacheIsReady) {
             $cacheContentLength = self::$_cache->getSize($fullCacheId);
         } else {
             // generate & cache content
             $content = self::_combineMinify();
             self::$_cache->store($cacheId, $content);
             if (function_exists('gzencode')) {
                 self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel']));
             }
         }
     } else {
         // no cache
         $cacheIsReady = false;
         $content = self::_combineMinify();
     }
     if (!$cacheIsReady && self::$_options['encodeMethod']) {
         // still need to encode
         $content = gzencode($content, self::$_options['encodeLevel']);
     }
     // add headers
     $headers['Content-Length'] = $cacheIsReady ? $cacheContentLength : strlen($content);
     $headers['Content-Type'] = self::$_options['contentTypeCharset'] ? self::$_options['contentType'] . '; charset=' . self::$_options['contentTypeCharset'] : self::$_options['contentType'];
     if (self::$_options['encodeMethod'] !== '') {
         $headers['Content-Encoding'] = $contentEncoding;
     }
     if (self::$_options['encodeOutput']) {
         $headers['Vary'] = 'Accept-Encoding';
     }
     if (!self::$_options['quiet']) {
         // output headers & content
         foreach ($headers as $name => $val) {
             header($name . ': ' . $val);
         }
         if ($cacheIsReady) {
             self::$_cache->display($fullCacheId);
         } else {
             echo $content;
         }
     } else {
         return array('success' => true, 'statusCode' => 200, 'content' => $cacheIsReady ? self::$_cache->fetch($fullCacheId) : $content, 'headers' => $headers);
     }
 }
 /**
  * Exit if the client's cache is valid for this resource
  *
  * This is a convenience method for common use of the class
  *
  * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers
  * will be sent with content. This is recommended.
  *
  * @param bool $isPublic (default false) if true, the Cache-Control header 
  * will contain "public", allowing proxies to cache the content. Otherwise 
  * "private" will be sent, allowing only browser caching.
  *
  * @param array $options (default empty) additional options for constructor
  *
  * @return null     
  */
 public static function check($lastModifiedTime = null, $isPublic = false, $options = array())
 {
     if (null !== $lastModifiedTime) {
         $options['lastModifiedTime'] = (int) $lastModifiedTime;
     }
     $options['isPublic'] = (bool) $isPublic;
     $cg = new Diglin_HTTP_ConditionalGet($options);
     $cg->sendHeaders();
     if ($cg->cacheIsValid) {
         exit;
     }
 }