2017-07-20 11:21:28 +02:00
using System ;
2014-12-18 12:45:04 +11:00
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
2015-06-15 17:13:34 +02:00
using System.Linq ;
2015-03-06 16:01:49 +11:00
using System.Xml ;
2014-12-18 12:45:04 +11:00
using System.Xml.Linq ;
using Umbraco.Core.Cache ;
2017-05-30 15:46:25 +02:00
using Umbraco.Core.Composing ;
2015-01-07 10:39:00 +11:00
using Umbraco.Core.Logging ;
2014-12-18 12:45:04 +11:00
2017-12-28 09:18:09 +01:00
namespace Umbraco.Core.Services.Implement
2014-12-18 12:45:04 +11:00
{
/// <summary>
/// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care of
/// </summary>
public class LocalizedTextServiceFileSources
{
2015-06-15 17:13:34 +02:00
private readonly ILogger _logger ;
2019-01-18 07:56:38 +01:00
private readonly IAppPolicyCache _cache ;
2015-06-15 17:13:34 +02:00
private readonly IEnumerable < LocalizedTextServiceSupplementaryFileSource > _supplementFileSources ;
2014-12-18 12:45:04 +11:00
private readonly DirectoryInfo _fileSourceFolder ;
2019-01-27 01:17:32 -05:00
// TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
2015-03-06 16:01:49 +11:00
private readonly Dictionary < string , CultureInfo > _twoLetterCultureConverter = new Dictionary < string , CultureInfo > ( ) ;
2015-03-20 18:15:31 +11:00
private readonly Lazy < Dictionary < CultureInfo , Lazy < XDocument > > > _xmlSources ;
2015-06-15 17:13:34 +02:00
/// <summary>
/// This is used to configure the file sources with the main file sources shipped with Umbraco and also including supplemental/plugin based
2017-07-20 11:21:28 +02:00
/// localization files. The supplemental files will be loaded in and merged in after the primary files.
2015-06-15 17:13:34 +02:00
/// The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
/// </summary>
/// <param name="logger"></param>
/// <param name="cache"></param>
/// <param name="fileSourceFolder"></param>
/// <param name="supplementFileSources"></param>
public LocalizedTextServiceFileSources (
ILogger logger ,
2019-01-17 11:19:06 +01:00
AppCaches appCaches ,
2015-06-15 17:13:34 +02:00
DirectoryInfo fileSourceFolder ,
2017-07-20 11:21:28 +02:00
IEnumerable < LocalizedTextServiceSupplementaryFileSource > supplementFileSources )
2014-12-18 12:45:04 +11:00
{
2015-06-15 17:13:34 +02:00
if ( logger = = null ) throw new ArgumentNullException ( "logger" ) ;
2019-01-17 11:19:06 +01:00
if ( appCaches = = null ) throw new ArgumentNullException ( "cache" ) ;
2014-12-18 12:45:04 +11:00
if ( fileSourceFolder = = null ) throw new ArgumentNullException ( "fileSourceFolder" ) ;
2015-03-20 18:15:31 +11:00
2015-06-15 17:13:34 +02:00
_logger = logger ;
2019-01-17 11:19:06 +01:00
_cache = appCaches . RuntimeCache ;
2015-01-07 10:39:00 +11:00
2015-03-20 18:15:31 +11:00
//Create the lazy source for the _xmlSources
_xmlSources = new Lazy < Dictionary < CultureInfo , Lazy < XDocument > > > ( ( ) = >
2015-01-07 10:39:00 +11:00
{
2015-03-20 18:15:31 +11:00
var result = new Dictionary < CultureInfo , Lazy < XDocument > > ( ) ;
2015-01-07 10:39:00 +11:00
2015-03-20 18:15:31 +11:00
if ( _fileSourceFolder = = null ) return result ;
2015-01-07 10:39:00 +11:00
2015-03-20 18:15:31 +11:00
foreach ( var fileInfo in _fileSourceFolder . GetFiles ( "*.xml" ) )
2015-03-06 16:01:49 +11:00
{
2015-03-20 18:15:31 +11:00
var localCopy = fileInfo ;
var filename = Path . GetFileNameWithoutExtension ( localCopy . FullName ) . Replace ( "_" , "-" ) ;
2019-01-27 01:17:32 -05:00
// TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
2015-03-20 18:15:31 +11:00
// names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we
// need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that
// if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea)
// that any 4 letter file is named with the actual culture that it is!
CultureInfo culture = null ;
if ( filename . Length = = 2 )
2015-03-06 16:01:49 +11:00
{
2015-03-20 18:15:31 +11:00
//we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't
//want to load in the entire doc into mem just to read a single value
using ( var fs = fileInfo . OpenRead ( ) )
using ( var reader = XmlReader . Create ( fs ) )
2015-03-06 16:01:49 +11:00
{
2015-03-20 18:15:31 +11:00
if ( reader . IsStartElement ( ) )
2015-03-06 16:01:49 +11:00
{
2015-03-20 18:15:31 +11:00
if ( reader . Name = = "language" )
2015-03-06 16:01:49 +11:00
{
2015-03-20 18:15:31 +11:00
if ( reader . MoveToAttribute ( "culture" ) )
2015-03-06 16:01:49 +11:00
{
2015-03-20 18:15:31 +11:00
var cultureVal = reader . Value ;
try
{
culture = CultureInfo . GetCultureInfo ( cultureVal ) ;
//add to the tracked dictionary
_twoLetterCultureConverter [ filename ] = culture ;
}
catch ( CultureNotFoundException )
{
2018-08-14 15:08:32 +01:00
Current . Logger . Warn < LocalizedTextServiceFileSources > ( "The culture {CultureValue} found in the file {CultureFile} is not a valid culture" , cultureVal , fileInfo . FullName ) ;
2015-03-20 18:15:31 +11:00
//If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise
// an exception will be thrown.
}
2015-03-06 16:01:49 +11:00
}
}
}
}
}
2015-03-20 18:15:31 +11:00
if ( culture = = null )
2014-12-18 12:45:04 +11:00
{
2015-03-20 18:15:31 +11:00
culture = CultureInfo . GetCultureInfo ( filename ) ;
}
2017-07-20 11:21:28 +02:00
//get the lazy value from cache
2015-04-27 13:40:41 +10:00
result [ culture ] = new Lazy < XDocument > ( ( ) = > _cache . GetCacheItem < XDocument > (
2015-06-15 17:13:34 +02:00
string . Format ( "{0}-{1}" , typeof ( LocalizedTextServiceFileSources ) . Name , culture . Name ) , ( ) = >
2014-12-18 12:45:04 +11:00
{
2015-06-15 17:13:34 +02:00
XDocument xdoc ;
//load in primary
2015-03-20 18:15:31 +11:00
using ( var fs = localCopy . OpenRead ( ) )
{
2015-06-15 17:13:34 +02:00
xdoc = XDocument . Load ( fs ) ;
2015-03-20 18:15:31 +11:00
}
2015-06-15 17:13:34 +02:00
//load in supplementary
MergeSupplementaryFiles ( culture , xdoc ) ;
return xdoc ;
} , isSliding : true , timeout : TimeSpan . FromMinutes ( 10 ) , dependentFiles : new [ ] { localCopy . FullName } ) ) ;
2015-03-20 18:15:31 +11:00
}
return result ;
} ) ;
if ( fileSourceFolder . Exists = = false )
{
2018-08-14 15:08:32 +01:00
Current . Logger . Warn < LocalizedTextServiceFileSources > ( "The folder does not exist: {FileSourceFolder}, therefore no sources will be discovered" , fileSourceFolder . FullName ) ;
2015-03-20 18:15:31 +11:00
}
else
{
2015-06-15 17:13:34 +02:00
_fileSourceFolder = fileSourceFolder ;
_supplementFileSources = supplementFileSources ;
2014-12-18 12:45:04 +11:00
}
2015-03-20 18:15:31 +11:00
}
2015-06-15 17:13:34 +02:00
/// <summary>
/// Constructor
/// </summary>
2019-01-17 11:19:06 +01:00
public LocalizedTextServiceFileSources ( ILogger logger , AppCaches appCaches , DirectoryInfo fileSourceFolder )
: this ( logger , appCaches , fileSourceFolder , Enumerable . Empty < LocalizedTextServiceSupplementaryFileSource > ( ) )
{ }
2015-06-15 17:13:34 +02:00
2015-03-20 18:15:31 +11:00
/// <summary>
/// returns all xml sources for all culture files found in the folder
/// </summary>
/// <returns></returns>
public IDictionary < CultureInfo , Lazy < XDocument > > GetXmlSources ( )
{
return _xmlSources . Value ;
2014-12-18 12:45:04 +11:00
}
2015-03-06 16:01:49 +11:00
2019-01-27 01:17:32 -05:00
// TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
2015-03-06 16:01:49 +11:00
public Attempt < CultureInfo > TryConvert2LetterCultureTo4Letter ( string twoLetterCulture )
{
2015-08-20 16:07:46 +02:00
if ( twoLetterCulture . Length ! = 2 ) return Attempt < CultureInfo > . Fail ( ) ;
2015-03-06 16:01:49 +11:00
2015-03-20 18:15:31 +11:00
//This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
var resolved = _xmlSources . Value ;
2015-03-06 16:01:49 +11:00
return _twoLetterCultureConverter . ContainsKey ( twoLetterCulture )
? Attempt . Succeed ( _twoLetterCultureConverter [ twoLetterCulture ] )
: Attempt < CultureInfo > . Fail ( ) ;
}
2015-06-15 17:13:34 +02:00
2019-01-27 01:17:32 -05:00
// TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
2015-08-20 16:07:46 +02:00
public Attempt < string > TryConvert4LetterCultureTo2Letter ( CultureInfo culture )
{
if ( culture = = null ) throw new ArgumentNullException ( "culture" ) ;
//This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
var resolved = _xmlSources . Value ;
return _twoLetterCultureConverter . Values . Contains ( culture )
? Attempt . Succeed ( culture . Name . Substring ( 0 , 2 ) )
: Attempt < string > . Fail ( ) ;
}
2015-06-15 17:13:34 +02:00
private void MergeSupplementaryFiles ( CultureInfo culture , XDocument xMasterDoc )
{
if ( xMasterDoc . Root = = null ) return ;
if ( _supplementFileSources ! = null )
{
2019-01-22 18:03:39 -05:00
//now load in supplementary
2015-06-15 17:13:34 +02:00
var found = _supplementFileSources . Where ( x = >
{
var fileName = Path . GetFileName ( x . File . FullName ) ;
return fileName . InvariantStartsWith ( culture . Name ) & & fileName . InvariantEndsWith ( ".xml" ) ;
} ) ;
2017-07-20 11:21:28 +02:00
2015-06-15 17:13:34 +02:00
foreach ( var supplementaryFile in found )
{
using ( var fs = supplementaryFile . File . OpenRead ( ) )
{
XDocument xChildDoc ;
try
{
xChildDoc = XDocument . Load ( fs ) ;
}
catch ( Exception ex )
{
2018-08-17 15:41:58 +01:00
_logger . Error < LocalizedTextServiceFileSources > ( ex , "Could not load file into XML {File}" , supplementaryFile . File . FullName ) ;
2015-06-15 17:13:34 +02:00
continue ;
}
if ( xChildDoc . Root = = null ) continue ;
foreach ( var xArea in xChildDoc . Root . Elements ( "area" )
. Where ( x = > ( ( string ) x . Attribute ( "alias" ) ) . IsNullOrWhiteSpace ( ) = = false ) )
{
var areaAlias = ( string ) xArea . Attribute ( "alias" ) ;
var areaFound = xMasterDoc . Root . Elements ( "area" ) . FirstOrDefault ( x = > ( ( string ) x . Attribute ( "alias" ) ) = = areaAlias ) ;
if ( areaFound = = null )
{
//add the whole thing
xMasterDoc . Root . Add ( xArea ) ;
}
else
{
MergeChildKeys ( xArea , areaFound , supplementaryFile . OverwriteCoreKeys ) ;
}
}
}
}
}
}
private void MergeChildKeys ( XElement source , XElement destination , bool overwrite )
{
if ( destination = = null ) throw new ArgumentNullException ( "destination" ) ;
if ( source = = null ) throw new ArgumentNullException ( "source" ) ;
//merge in the child elements
foreach ( var key in source . Elements ( "key" )
. Where ( x = > ( ( string ) x . Attribute ( "alias" ) ) . IsNullOrWhiteSpace ( ) = = false ) )
{
var keyAlias = ( string ) key . Attribute ( "alias" ) ;
var keyFound = destination . Elements ( "key" ) . FirstOrDefault ( x = > ( ( string ) x . Attribute ( "alias" ) ) = = keyAlias ) ;
if ( keyFound = = null )
{
//append, it doesn't exist
destination . Add ( key ) ;
}
else if ( overwrite )
{
//overwrite
keyFound . Value = key . Value ;
}
}
}
2014-12-18 12:45:04 +11:00
}
2017-07-20 11:21:28 +02:00
}