/// <summary>
        /// This is only exposed for unit testing. This will throw an exception if unable to generate the content, it will never return null or a blank string.
        /// </summary>
        public static string GetFileContentRepresentation(TextFileContents contents, long millisecondsTakenToGenerate)
        {
            if (contents == null)
            {
                throw new ArgumentNullException("contents");
            }
            if (millisecondsTakenToGenerate < 0)
            {
                throw new ArgumentOutOfRangeException("millisecondsTakenToGenerate", "may not be a negative value");
            }

            var contentBuilder = new StringBuilder();

            contentBuilder.AppendFormat(
                "/*{0}:{1}:{2}:{3}ms*/{4}",
                contents.RelativePath.Length.ToString(
                    new string('0', int.MaxValue.ToString().Length)                     // Pad out the length to the number of digits required to display int.MaxValue
                    ),
                contents.RelativePath,
                contents.LastModified.ToString(LastModifiedDateFormat),
                Math.Min(millisecondsTakenToGenerate, 99999).ToString("00000"),
                Environment.NewLine
                );
            contentBuilder.Append(contents.Content);
            return(contentBuilder.ToString());
        }
        /// <summary>
        /// This will never return null, it will throw an exception for a null or empty relativePath - it is up to the particular implementation whether or not to throw
        /// an exception for invalid / inaccessible filenames (if no exception is thrown, the issue should be logged). It is up the the implementation to handle mapping
        /// the relative path to a full file path.
        /// </summary>
        public TextFileContents Load(string relativePath)
        {
            if (string.IsNullOrWhiteSpace(relativePath))
            {
                throw new ArgumentException("Null/blank relativePath specified");
            }

            var file = new FileInfo(_relativePathMapper.MapPath(relativePath));

            if (!file.Exists)
            {
                throw new ArgumentException("Requested file does not exist: " + relativePath);
            }

            try
            {
                var fileContents = new TextFileContents(
                    relativePath,
                    file.LastWriteTime,
                    File.ReadAllText(file.FullName)
                    );
                return(fileContents);
            }
            catch (Exception e)
            {
                throw new ArgumentException("Unable to load requested file: " + relativePath, e);
            }
        }
        private TextFileContents RemoveCSSComments(TextFileContents content)
        {
            if (content == null)
            {
                throw new ArgumentNullException("content");
            }

            return(new TextFileContents(
                       content.RelativePath,
                       content.LastModified,
                       CSSCommentRemover.Replace(content.Content + "/**/", "")          // Ensure that any unclosed comments are handled
                       ));
        }
        /// <summary>
        /// This will never return null, it will throw an exception for a null or empty relativePath - it is up to the particular implementation whether or not to throw
        /// an exception for invalid / inaccessible filenames (if no exception is thrown, the issue should be logged). It is up the the implementation to handle mapping
        /// the relative path to a full file path.
        /// </summary>
        public TextFileContents Load(string relativePath)
        {
            if (string.IsNullOrWhiteSpace(relativePath))
            {
                throw new ArgumentException("Null/blank relativePath specified");
            }

            // Try to retrieve cached data
            var lastModifiedDateOfSource = _lastModifiedDateRetriever.GetLastModifiedDate(relativePath);
            var cacheFile = _cacheFileLocationRetriever(relativePath);

            cacheFile.Refresh();
            if (cacheFile.Exists)
            {
                try
                {
                    using (var stream = cacheFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        using (var reader = new StreamReader(stream))
                        {
                            var cachedData = GetFileContents(reader);
                            if (cachedData.LastModified >= lastModifiedDateOfSource)
                            {
                                return(cachedData);
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    _logger.LogIgnoringAnyError(LogLevel.Error, () => "DiskCachingTextFileLoader.Load: Error loading content - " + e.Message);
                    if (e is InvalidCacheFileFormatException)
                    {
                        if (_invalidContentBehaviour == InvalidContentBehaviourOptions.Delete)
                        {
                            try
                            {
                                cacheFile.Delete();
                            }
                            catch (Exception invalidFileContentDeleteException)
                            {
                                _logger.LogIgnoringAnyError(
                                    LogLevel.Warning,
                                    () => "DiskCachingTextFileLoader.Add: Unable to delete cache file with invalid contents - " + invalidFileContentDeleteException.Message,
                                    invalidFileContentDeleteException
                                    );
                                if (_errorBehaviour == ErrorBehaviourOptions.LogAndRaiseException)
                                {
                                    throw;
                                }
                            }
                        }
                    }
                    else
                    {
                        if (_errorBehaviour == ErrorBehaviourOptions.LogAndRaiseException)
                        {
                            throw;
                        }
                    }
                }
            }

            // Do the work and cache the result
            var timer = new Stopwatch();

            timer.Start();
            var content = _contentLoader.Load(relativePath);

            timer.Stop();
            try
            {
                // 2015-12-22 DWR: The contentLoader may have a sensible-seeming LastModified-retrieval mechanism of take-largest-last-modified-date-from-the-source-
                // files but we want to ignore that value if it is less recent than the value that the lastModifiedDateRetriever reference gave us, otherwise the
                // cached data will be useless. It's easier to explain with an example - if Style1.css, Style11.css, Style12.css and Style2.css are all in the same
                // folder and Style1.css imports Style11.css and Style12.css while Style11.css, Style12.css and Style.css import nothing, the disk-cache data for
                // Style1 will be useless if Style2 is updated and the lastModifiedDateRetriever is a SingleFolderLastModifiedDateRetriever. This is because the
                // lastModifiedDateRetriever will return the last-modified date of Style2, which will always be more recent than any of Style1, Style11, Style12
                // last-modified values but the last-modified of the flattened-Style1.css content will only consider the last-modified dates of the three files
                // that it pulls data from. It is not as accurate to claim that the flattened-Style1.css content has the last-modified date of Style2 but it's
                // the only way to get consistent dates, which are required for the caching to work effectively.
                if (lastModifiedDateOfSource > content.LastModified)
                {
                    content = new TextFileContents(content.RelativePath, lastModifiedDateOfSource, content.Content);
                }
                File.WriteAllText(
                    cacheFile.FullName,
                    GetFileContentRepresentation(content, timer.ElapsedMilliseconds)
                    );
            }
            catch (Exception e)
            {
                _logger.LogIgnoringAnyError(LogLevel.Warning, () => "DiskCachingTextFileLoader.Add: Error writing file - " + e.Message, e);
                if (_errorBehaviour == ErrorBehaviourOptions.LogAndRaiseException)
                {
                    throw;
                }
            }
            return(content);
        }
        /// <summary>
        /// This will return a TextFileContents instance creating by removing all comments and flattening all of the import declarations in a stylesheet - nested
        /// imports are handled recursively. Only imports in the same folder are supported - the imports may not have relative or absolute paths specified, nor
        /// may they be external urls - breaking these conditions will result in an UnsupportedStylesheetImportException being raised. If there are any circular
        /// references defined by the imports, a CircularStylesheetImportException will be raised. The LastModified value on the returned data will be the most
        /// recent date taken from all of the considered files.
        /// </summary>
        private TextFileContents GetCombinedContent(string relativePath, IEnumerable <string> importChain)
        {
            if (string.IsNullOrWhiteSpace(relativePath))
            {
                throw new ArgumentException("Null/blank relativePath specified");
            }
            if (importChain == null)
            {
                throw new ArgumentNullException("importChain");
            }

            var importChainArray = importChain.ToList();

            if (importChainArray.Any(i => string.IsNullOrWhiteSpace(i)))
            {
                throw new ArgumentException("Null/blank entry encountered in importChain set");
            }

            var combinedContentFile = _contentLoader.Load(relativePath);

            if (_contentLoaderCommentRemovalBehaviour == ContentLoaderCommentRemovalBehaviourOptions.ContentIsUnprocessed)
            {
                combinedContentFile = RemoveCSSComments(combinedContentFile);
            }
            foreach (var importDeclaration in GetImportDeclarations(combinedContentFile.Content))
            {
                if (importDeclaration == null)
                {
                    throw new Exception("Null reference encountered in data returned from GetImportDeclarations");
                }

                // Ensure that the imported stylesheet is not a relative or absolute path or an external url
                var removeImport = false;
                if (importDeclaration.RelativePath.Contains("\\") || importDeclaration.RelativePath.Contains("/"))
                {
                    if (_unsupportedImportBehaviour == ErrorBehaviourOptions.LogAndContinue)
                    {
                        _logger.LogIgnoringAnyError(LogLevel.Warning, () => "Unsupported import specified: " + importDeclaration.RelativePath + " (it has been removed)");
                        removeImport = true;
                    }
                    else
                    {
                        _logger.LogIgnoringAnyError(LogLevel.Warning, () => "Unsupported import specified: " + importDeclaration.RelativePath);
                        throw new UnsupportedStylesheetImportException("Imported stylesheets may not specify relative or absolute paths nor external urls: " + importDeclaration.RelativePath);
                    }
                }

                // If the original file has a relative path (eg. "styles/Test1.css") then we'll need to include that path in the import filename (eg. "Test2.css"
                // must be transformed for "styles/Test2.css") otherwise the circular reference detection won't work and the file won't be loaded from the right
                // location when GetCombinedContent is called recursively for the import. We can use this approach to include the path on the import filename
                // since we are only supporting imports in the same location as the containing stylesheet (see above; relative or absolute paths are not
                // allowed in imports)
                StylesheetImportDeclaration importDeclarationWithConsistentFilename;
                var breakPoint = relativePath.LastIndexOfAny(new[] { '\\', '/' });
                if (breakPoint == -1)
                {
                    importDeclarationWithConsistentFilename = importDeclaration;
                }
                else
                {
                    importDeclarationWithConsistentFilename = new StylesheetImportDeclaration(
                        importDeclaration.Declaration,
                        relativePath.Substring(0, breakPoint + 1) + importDeclaration.RelativePath,
                        importDeclaration.MediaOverride
                        );
                }

                // Ensure that the requested stylesheet has not been requested further up the chain - if so, throw a CircularStylesheetImportException rather than
                // waiting for a StackOverflowException to occur (or log a warning and remove the import, depending upon specified behaviour options)
                if (importChainArray.Any(f => f.Equals(importDeclarationWithConsistentFilename.RelativePath, StringComparison.InvariantCultureIgnoreCase)))
                {
                    if (_circularReferenceImportBehaviour == ErrorBehaviourOptions.LogAndContinue)
                    {
                        _logger.LogIgnoringAnyError(
                            LogLevel.Warning,
                            () => string.Format(
                                "Circular import encountered: {0} (it has been removed from {1})",
                                importDeclarationWithConsistentFilename.RelativePath,
                                relativePath
                                )
                            );
                        removeImport = true;
                    }
                    else
                    {
                        throw new CircularStylesheetImportException("Circular stylesheet import detected for file: " + importDeclarationWithConsistentFilename.RelativePath);
                    }
                }

                // Retrieve the content from imported file, wrap it in a media query if required and replace the import declaration with the content
                TextFileContents importedFileContent;
                if (removeImport)
                {
                    // If we want to ignore this import (meaning it's invalid and DifferentFolderImportBehaviourOptions is to log and proceed instead of throw an
                    // exception) then we just want to replace the dodgy import with blank content
                    importedFileContent = new TextFileContents(importDeclarationWithConsistentFilename.RelativePath, DateTime.MinValue, "");
                }
                else
                {
                    importedFileContent = GetCombinedContent(
                        importDeclarationWithConsistentFilename.RelativePath,
                        importChainArray.Concat(new[] { combinedContentFile.RelativePath })
                        );
                }
                if ((importDeclarationWithConsistentFilename.MediaOverride != null) && !removeImport)                 // Don't bother wrapping an import that will be ignored in any media query content
                {
                    importedFileContent = new TextFileContents(
                        importedFileContent.RelativePath,
                        importedFileContent.LastModified,
                        String.Format(
                            "@media {0} {{{1}{2}{1}}}{1}",
                            importDeclarationWithConsistentFilename.MediaOverride,
                            Environment.NewLine,
                            importedFileContent.Content
                            )
                        );
                }
                combinedContentFile = new TextFileContents(
                    combinedContentFile.RelativePath,
                    combinedContentFile.LastModified > importedFileContent.LastModified ? combinedContentFile.LastModified : importedFileContent.LastModified,
                    combinedContentFile.Content.Replace(
                        importDeclarationWithConsistentFilename.Declaration,
                        importedFileContent.Content
                        )
                    );
            }
            return(combinedContentFile);
        }