2010-04-10 21:53:53 +00:00
// The Antville Project
// http://code.google.com/p/antville
//
2014-07-04 17:16:51 +02:00
// Copyright 2001– 2014 by the Workers of Antville.
2010-04-10 21:53:53 +00:00
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
2014-07-04 15:32:18 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2010-04-10 21:53:53 +00:00
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/ * *
2012-05-19 17:39:37 +00:00
* @ fileOverview Defines the Exporter namespace .
2010-04-10 21:53:53 +00:00
* /
2024-06-15 15:07:09 +02:00
// This code requires the Gson Java library bundled with Helma
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
global . Exporter = ( function ( ) {
2018-05-19 12:38:20 +02:00
const gson = new JavaImporter (
Packages . com . google . gson ,
Packages . com . google . gson . stream
) ;
const getJsonWriter = ( dir , fname ) => {
const file = new java . io . File ( dir , fname ) ;
const stream = new java . io . FileOutputStream ( file ) ;
const writer = new java . io . OutputStreamWriter ( stream , 'utf-8' ) ;
const jsonWriter = new gson . JsonWriter ( writer ) ;
jsonWriter . beginArray ( ) ;
return {
push ( data ) {
jsonWriter . jsonValue ( JSON . stringify ( data ) ) ;
return this ;
} ,
close ( ) {
jsonWriter . endArray ( ) ;
jsonWriter . flush ( ) ;
jsonWriter . close ( ) ;
writer . close ( ) ;
stream . close ( ) ;
return this ;
}
} ;
} ;
2018-05-11 12:56:48 +02:00
const addMetadata = ( object , Prototype ) => {
object . metadata = { } ;
const sql = new Sql ( ) ;
sql . retrieve ( "select name, value, type from metadata where parent_type = '$0' and parent_id = $1 order by lower(name)" , Prototype . name , object . id ) ;
sql . traverse ( function ( ) {
object . metadata [ this . name ] = global [ this . type ] ( this . value ) . valueOf ( ) ;
} ) ;
return object ;
} ;
2008-12-14 15:32:17 +00:00
2018-05-19 12:38:20 +02:00
const addImage = function ( type , writer ) {
2018-05-11 12:56:48 +02:00
app . log ( 'Exporting ' + type + ' image #' + this . id ) ;
const image = Image . getById ( this . id ) ;
if ( image ) {
this . href = image . href ( ) ;
addMetadata ( this , Image ) ;
2018-05-19 12:38:20 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} else {
app . logger . warn ( 'Could not export Image #' + this . id + '; might be a cache problem' ) ;
}
} ;
const addAssets = ( site , zip ) => {
const dir = site . getStaticFile ( ) ;
2018-05-19 12:38:20 +02:00
if ( dir . exists ( ) ) zip . add ( dir , 'static' ) ;
2018-05-11 12:56:48 +02:00
} ;
/ * *
* The Exporter namespace provides methods for exporting a site .
* @ namespace
* /
const Exporter = { }
/ * *
* Exports a site with the specified user ’ s content
* The created XML file will be added to the site ’ s file collection .
* @ param { Site } site The site to export .
* @ param { User } user The user whose content will be exported .
* /
Exporter . run = function ( target , user ) {
switch ( target . constructor ) {
case Site :
Exporter . saveSite ( target , user ) ;
break ;
case User :
Exporter . saveAccount ( target ) ;
break ;
2014-07-04 15:32:18 +02:00
}
2018-05-11 12:56:48 +02:00
} ;
Exporter . saveSite = function ( site , user ) {
const sql = new Sql ( ) ;
const zip = new helma . Zip ( ) ;
2018-05-20 08:52:07 +02:00
const dirName = app . appsProperties [ 'static' ] + '/export' ;
const fileName = 'antville-site-' + java . util . UUID . randomUUID ( ) + '.zip' ;
2018-05-20 09:42:19 +02:00
const dir = new java . io . File ( dirName ) ;
const file = new java . io . File ( dir , fileName ) ;
if ( ! dir . exists ( ) ) dir . mkdirs ( ) ;
2018-05-11 12:56:48 +02:00
2018-05-20 08:52:07 +02:00
if ( site . export ) {
const archive = new java . io . File ( dirName , site . export . split ( '/' ) . pop ( ) ) ;
if ( archive . exists ( ) ) archive [ 'delete' ] ( ) ;
}
2018-05-11 12:56:48 +02:00
2018-05-20 08:52:07 +02:00
try {
2018-05-20 09:42:19 +02:00
const tempDir = new java . io . File ( java . nio . file . Files . createTempDirectory ( site . name ) ) ;
const skinWriter = getJsonWriter ( tempDir , 'skins.json' ) ;
let writer = getJsonWriter ( tempDir , 'index.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select s.*, c.name as creator_name, m.name as modifier_name from site s, account c, account m where s.id = $0 and s.creator_id = c.id and s.modifier_id = m.id order by lower(s.name)' , site . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting site #' + this . id + ' (' + this . name + ')' ) ;
const site = Site . getById ( this . id ) ;
this . href = site . href ( ) ;
addAssets ( site , zip ) ;
addMetadata ( this , Site ) ;
2018-05-19 12:38:20 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
const skinsSql = new Sql ( ) ;
sql . retrieve ( 'select * from skin where site_id = $0' , this . id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting skin #' + this . id ) ;
2018-05-19 12:38:20 +02:00
skinWriter . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
} ) ;
2018-05-19 12:38:20 +02:00
writer . close ( ) ;
skinWriter . close ( ) ;
2018-05-20 09:42:19 +02:00
writer = getJsonWriter ( tempDir , 'members.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select m.*, c.name as creator_name, mod.name as modifier_name from site s, membership m, account c, account mod where s.id = $0 and s.id = m.site_id and m.creator_id = c.id and m.modifier_id = mod.id order by lower(m.name)' , site . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting membership #' + this . creator _id ) ;
2018-05-19 12:38:20 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-19 12:38:20 +02:00
writer . close ( ) ;
2018-05-20 09:42:19 +02:00
const storyWriter = getJsonWriter ( tempDir , 'stories.json' ) ;
const commentWriter = getJsonWriter ( tempDir , 'comments.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select c.*, crt.name as creator_name, m.name as modifier_name from content c, account crt, account m where c.site_id = $0 and c.creator_id = crt.id and c.modifier_id = m.id order by created desc' , site . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting story #' + this . id ) ;
const content = Story . getById ( this . id ) ;
this . href = content . href ( ) ;
addMetadata ( this , Story ) ;
this . rendered = content . format _filter ( this . metadata . text , { } , 'markdown' ) ;
if ( this . prototype === 'Story' ) {
2018-05-19 12:38:20 +02:00
storyWriter . push ( this ) ;
2018-05-11 12:56:48 +02:00
} else {
2018-05-19 12:38:20 +02:00
commentWriter . push ( this ) ;
2018-05-11 12:56:48 +02:00
}
} ) ;
2018-05-19 12:38:20 +02:00
storyWriter . close ( ) ;
commentWriter . close ( ) ;
2018-05-20 09:42:19 +02:00
writer = getJsonWriter ( tempDir , 'files.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select f.*, c.name as creator_name, m.name as modifier_name from file f, account c, account m where f.site_id = $0 and f.creator_id = c.id and f.modifier_id = m.id order by created desc' , site . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting file #' + this . id ) ;
const file = File . getById ( this . id ) ;
this . href = file . href ( ) ;
addMetadata ( this , File ) ;
2018-05-19 12:38:20 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-19 12:38:20 +02:00
writer . close ( ) ;
2018-05-20 09:42:19 +02:00
writer = getJsonWriter ( tempDir , 'images.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
sql . retrieve ( "select i.*, c.name as creator_name, m.name as modifier_name from image i, account c, account m where i.parent_type = 'Site' and i.parent_id = $0 and i.creator_id = c.id and i.modifier_id = m.id order by created desc" , site . _id ) ;
sql . traverse ( function ( ) {
2018-05-19 12:38:20 +02:00
addImage . call ( this , 'site' , writer ) ;
2018-05-11 12:56:48 +02:00
} ) ;
sql . retrieve ( "select i.*, c.name as creator_name, m.name as modifier_name from image i, layout l, account c, account m where i.parent_type = 'Layout' and i.parent_id = l.id and l.site_id = $0 and i.creator_id = c.id and i.modifier_id = m.id order by created desc" , site . _id ) ;
sql . traverse ( function ( ) {
2018-05-19 12:38:20 +02:00
addImage . call ( this , 'layout' , writer ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-19 12:38:20 +02:00
writer . close ( ) ;
2018-05-20 09:42:19 +02:00
writer = getJsonWriter ( tempDir , 'polls.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select p.*, c.name as creator_name, m.name as modifier_name from poll p, account c, account m where p.site_id = $0 and p.creator_id = c.id and p.modifier_id = m.id order by created desc' , site . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting poll #' + this . id ) ;
const poll = Poll . getById ( this . id ) ;
this . href = poll . href ( ) ;
this . choices = poll . list ( ) . map ( choice => {
return {
id : choice . _id ,
title : choice . title ,
votes : choice . size ( )
} ;
} ) ;
addMetadata ( this , Poll ) ;
2018-05-19 12:38:20 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-19 12:38:20 +02:00
writer . close ( ) ;
2018-05-20 09:42:19 +02:00
writer = getJsonWriter ( tempDir , 'tags.json' ) ;
2018-05-19 12:38:20 +02:00
2018-05-11 15:02:45 +02:00
sql . retrieve ( 'select t.name, h.* from tag t, tag_hub h where t.id = h.tag_id order by t.name' ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting tag #' + this . id ) ;
2018-05-19 12:38:20 +02:00
writer . push ( this ) ;
2018-05-11 15:02:45 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( ) ;
2018-05-11 14:20:53 +02:00
const xml = Exporter . getSiteXml ( site ) ;
2018-05-11 12:56:48 +02:00
2018-05-20 08:52:07 +02:00
zip . addData ( xml , 'export.xml' ) ;
2018-05-20 09:42:19 +02:00
zip . add ( tempDir ) ;
2018-05-20 08:52:07 +02:00
zip . save ( file ) ;
2018-05-11 12:56:48 +02:00
2018-05-20 08:52:07 +02:00
site . export = app . appsProperties . staticMountpoint + '/export/' + fileName ;
site . job = null ;
2018-05-11 12:56:48 +02:00
} catch ( ex ) {
app . log ( ex . rhinoException ) ;
}
return ;
} ;
Exporter . saveAccount = account => {
const zip = new helma . Zip ( ) ;
const sql = new Sql ( ) ;
const dirName = app . appsProperties [ 'static' ] + '/export' ;
const fileName = 'antville-account-' + java . util . UUID . randomUUID ( ) + '.zip' ;
const dir = new java . io . File ( dirName ) ;
const file = new java . io . File ( dir , fileName ) ;
if ( ! dir . exists ( ) ) dir . mkdirs ( ) ;
if ( account . export ) {
const archive = new java . io . File ( dirName , account . export . split ( '/' ) . pop ( ) ) ;
if ( archive . exists ( ) ) archive [ 'delete' ] ( ) ;
}
2018-05-20 09:42:19 +02:00
const tempDir = new java . io . File ( java . nio . file . Files . createTempDirectory ( account . name ) ) ;
let writer = getJsonWriter ( tempDir , 'index.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( "select * from account where id = $0" , account . _id ) ;
// Cannot really include other accounts with the same e-mail address because we do not verify e-mail addresses
//sql.retrieve("select * from account where email = '$0' order by lower(name)", account.email);
sql . traverse ( function ( ) {
app . log ( 'Exporting account #' + this . id + ' (' + this . name + ')' ) ;
addMetadata ( this , User ) ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( ) ;
writer = getJsonWriter ( tempDir , 'sites.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( "select s.*, m.role, c.name as creator_name, mod.name as modifier_name from site s, membership m, account c, account mod where m.creator_id = $0 and m.site_id = s.id and s.creator_id = c.id and s.modifier_id = mod.id order by lower(s.name)" , account . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting site #' + this . id + ' (' + this . name + ')' ) ;
const site = Site . getById ( this . id ) ;
this . href = site . href ( ) ;
if ( this . role === Membership . OWNER ) addMetadata ( this , Site ) ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( ) ;
writer = getJsonWriter ( tempDir , 'skins.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select s.*, m.name as modifier_name from skin s, account m where s.creator_id = $0 and s.modifier_id = m.id' , account . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting skin #' + this . id ) ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( ) ;
writer = getJsonWriter ( tempDir , 'memberships.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select m.*, mod.name as modifier_name from site s, membership m, account mod where m.creator_id = $0 and s.id = m.site_id and m.modifier_id = mod.id order by lower(m.name)' , account . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting membership #' + this . id ) ;
this . creator _name = account . name ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( ) ;
writer = getJsonWriter ( tempDir , 'stories.json' ) ;
const commentWriter = getJsonWriter ( tempDir , 'comments.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select c.*, m.name as modifier_name from content c, account m where creator_id = $0 and c.modifier_id = m.id order by c.created desc' , account . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting story #' + this . id ) ;
const content = Story . getById ( this . id ) ;
this . href = content . href ( ) ;
this . creator _name = account . name ;
addMetadata ( this , Story ) ;
this . rendered = content . format _filter ( this . metadata . text , { } , 'markdown' ) ;
if ( this . prototype === 'Story' ) {
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} else {
2018-05-20 09:42:19 +02:00
commentWriter . push ( this ) ;
2018-05-11 12:56:48 +02:00
}
} ) ;
2018-05-20 09:42:19 +02:00
commentWriter . close ( ) ;
writer . close ( )
writer = getJsonWriter ( tempDir , 'files.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select f.*, m.name as modifier_name from file f, account m where f.creator_id = $0 and f.modifier_id = m.id order by f.created desc' , account . _id ) ;
2014-07-04 15:10:47 +02:00
2018-05-11 12:56:48 +02:00
sql . traverse ( function ( ) {
app . log ( 'Exporting file #' + this . id ) ;
const file = File . getById ( this . id ) ;
const asset = file . getFile ( ) ;
if ( asset . exists ( ) ) zip . add ( asset , file . site . name + '/files' ) ;
this . href = file . href ( ) ;
this . creator _name = account . name ;
addMetadata ( this , File ) ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( )
writer = getJsonWriter ( tempDir , 'images.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select i.*, m.name as modifier_name from image i, account m where i.creator_id = $0 and i.modifier_id = m.id order by i.created desc' , account . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting image #' + this . id ) ;
const image = Image . getById ( this . id ) ;
if ( image ) {
try {
const asset = image . getFile ( ) ;
const path = this . parent _type === 'Layout' ? image . parent . site . name + '/layout' : image . parent . name + '/images' ;
if ( asset . exists ( ) ) zip . add ( asset , path ) ;
} catch ( ex ) {
console . warn ( 'Could not export image #' + this . id ) ;
console . warn ( ex . rhinoException ) ;
}
this . href = image . href ( ) ;
this . creator _name = account . name ;
addMetadata ( this , Image ) ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} else {
app . logger . warn ( 'Could not export Image #' + this . id + '; might be a cache problem' ) ;
}
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( )
writer = getJsonWriter ( tempDir , 'polls.json' ) ;
2018-05-11 12:56:48 +02:00
sql . retrieve ( 'select p.*, m.name as modifier_name from poll p, account m where p.creator_id = $0 and p.modifier_id = m.id order by p.created desc' , account . _id ) ;
sql . traverse ( function ( ) {
app . log ( 'Exporting poll #' + this . id ) ;
const poll = Poll . getById ( this . id ) ;
this . href = poll . href ( ) ;
this . creator _name = account . name ;
this . choices = poll . list ( ) . map ( choice => {
return {
id : choice . _id ,
title : choice . title ,
votes : choice . size ( )
} ;
} ) ;
const vote = poll . votes . get ( account . name ) ;
if ( vote ) this . vote = vote . choice . _id ;
addMetadata ( this , Poll ) ;
2018-05-20 09:42:19 +02:00
writer . push ( this ) ;
2018-05-11 12:56:48 +02:00
} ) ;
2018-05-20 09:42:19 +02:00
writer . close ( ) ;
zip . add ( tempDir ) ;
2018-05-11 12:56:48 +02:00
zip . save ( file ) ;
account . export = app . appsProperties . staticMountpoint + '/export/' + fileName ;
account . job = null ;
return zip ;
} ;
2018-05-11 14:20:53 +02:00
Exporter . getSiteXml = site => {
const rssUrl = site . href ( 'rss.xml' ) ;
const xml = [ ] ;
2014-07-04 15:10:47 +02:00
2018-05-11 14:20:53 +02:00
const add = function ( s ) {
2015-05-25 22:05:11 +02:00
return xml . push ( s ) ;
} ;
2010-04-23 22:52:09 +00:00
2014-07-04 15:32:18 +02:00
add ( '<?xml version="1.0" encoding="UTF-8"?>' ) ;
add ( '<?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?>' ) ;
add ( '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:thr="http://purl.org/syndication/thread/1.0">' ) ;
add ( '<id>tag:blogger.com,1999:blog-' + site . _id + '.archive</id>' ) ;
add ( '<updated>' + site . modified . format ( Date . ISOFORMAT ) + '</updated>' ) ;
add ( '<title type="text">' + encodeXml ( site . title ) + '</title>' ) ;
add ( '<link rel="http://schemas.google.com/g/2005#feed" type="application/rss+xml" href="' + rssUrl + '"/>' ) ;
add ( '<link rel="self" type="application/rss+xml" href="' + rssUrl + '"/>' ) ;
add ( '<link rel="http://schemas.google.com/g/2005#post" type="application/rss+xml" href="' + rssUrl + '"/>' ) ;
add ( '<link rel="alternate" type="text/html" href="' + site . href ( ) + '"/>' ) ;
add ( '<author>' ) ;
add ( '<name>' + site . creator . name + '</name>' ) ;
add ( '<email>' + site . creator . email + '</email>' ) ;
add ( '</author>' ) ;
// Currently, blogger.com does not accept other generators
//add('<generator version="' + Root.VERSION + '" uri="' + root.href() + '">Antville</generator>');
add ( '<generator version="7.00" uri="http://www.blogger.com">Blogger</generator>' ) ;
2018-05-11 14:20:53 +02:00
site . stories . forEach ( function ( ) {
2014-07-04 15:32:18 +02:00
add ( '<entry>' ) ;
add ( '<id>tag:blogger.com,1999:blog-' + site . _id + '.post-' + this . _id + '</id>' ) ;
add ( '<published>' + this . created . format ( Date . ISOFORMAT ) + '</published>' ) ;
add ( '<updated>' + this . modified . format ( Date . ISOFORMAT ) + '</updated>' ) ;
add ( '<title type="text">' + ( this . title ? encodeXml ( this . title . stripTags ( ) ) : '' ) + '</title>' ) ;
add ( '<content type="html">' + encodeXml ( this . format _filter ( this . text , { } ) ) + '</content>' ) ;
add ( '<link rel="alternate" type="text/html" href="' + this . href ( ) + '"></link>' ) ;
add ( '<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/blogger/2008/kind#post"/>' ) ;
2010-04-20 16:29:10 +00:00
add ( '<author>' ) ;
2014-07-04 15:32:18 +02:00
add ( '<name>' + this . creator . name + '</name>' ) ;
2018-05-11 14:20:53 +02:00
if ( this . creator . url ) add ( '<uri>' + this . creator . url + '</uri>' ) ;
2014-07-04 15:32:18 +02:00
add ( '<email>' + this . creator . email + '</email>' ) ;
2010-04-20 16:29:10 +00:00
add ( '</author>' ) ;
2014-07-04 15:32:18 +02:00
add ( '</entry>' ) ;
} ) ;
add ( '</feed>' ) ;
2010-04-23 22:52:09 +00:00
2018-05-11 12:56:48 +02:00
return java . lang . String ( xml . join ( String . EMPTY ) ) . getBytes ( 'utf-8' ) ;
2018-05-04 17:02:00 +02:00
} ;
2018-05-11 12:56:48 +02:00
return Exporter ;
} ) ( ) ;