Browse Source

Add support to define the stylesheet as a resource in <xslt>

Bugzilla Report 39407


git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@409378 13f79535-47bb-0310-9956-ffa450edef68
master
Antoine Levy-Lambert 19 years ago
parent
commit
88f1be2d04
7 changed files with 304 additions and 88 deletions
  1. +3
    -0
      WHATSNEW
  2. +19
    -1
      docs/manual/CoreTasks/style.html
  3. +22
    -0
      src/etc/testcases/taskdefs/style/build.xml
  4. +34
    -0
      src/main/org/apache/tools/ant/taskdefs/XSLTLiaison3.java
  5. +137
    -73
      src/main/org/apache/tools/ant/taskdefs/XSLTProcess.java
  6. +57
    -8
      src/main/org/apache/tools/ant/taskdefs/optional/TraXLiaison.java
  7. +32
    -6
      src/testcases/org/apache/tools/ant/taskdefs/StyleTest.java

+ 3
- 0
WHATSNEW View File

@@ -419,6 +419,9 @@ Other changes:

* <scp> now optionally supports the sftp protocol. Bugzilla Report 39373.

* resources can now be used to indicate the location of the stylesheet to use
in <xslt>. Bugzilla Report 39407.

Changes from Ant 1.6.4 to Ant 1.6.5
===================================



+ 19
- 1
docs/manual/CoreTasks/style.html View File

@@ -82,12 +82,15 @@ element which is used to perform Entity and URI resolution.</p>
<td valign="top">name of the stylesheet to use - given either relative
to the project's basedir or as an absolute path.<br/>
<br/>
Alternatively, a nested element which ant can interpret as a resource
can be used to indicate where to find the stylesheet<br/>
<em>deprecated variation :</em> <br/>
If the stylesheet cannot be found, and if you have specified the
attribute basedir for the task, ant will assume that the style
attribute is relative to the basedir of the task.
</td>
<td align="center" valign="top">Yes</td>
<td align="center" valign="top">No, if you specify the location of
the stylesheet as a nested resource element</td>
</tr>
<tr>
<td valign="top">classpath</td>
@@ -348,6 +351,13 @@ used by <code>&lt;xslt&gt;</code> removes the file extension from the
source file and adds the extension specified via the extension
attribute.</p>

<h4>nested element of type resource to indicate the stylesheet</h4>

<p><em>Since Ant 1.7</em></p>

<p>You can use nested elements which extend resource to indicate the stylesheet.
See <a href="../CoreTypes/resources.html">resources</a> to see the concrete syntax you can use</p>

<h3>Examples</h3>
<blockquote>
<pre>
@@ -407,6 +417,14 @@ attribute.</p>
style=&quot;style/apache.xsl&quot;&gt;
&lt;mapper type=&quot;glob&quot; from=&quot;*.xml.en&quot; to=&quot;*.html.en&quot;/&gt;
&lt;/xslt&gt;</pre>

<h4>Using a nested resource to define the stylesheet</h4>
<pre>
&lt;xslt in="data.xml" out="${out.dir}/out.xml"&gt;
&lt;url url="${printParams.xsl.url}"/&gt;
&lt;param name="set" expression="value"/&gt;
&lt;/xslt&gt;</pre>

</blockquote>
<hr>
<p align="center">Copyright &copy; 2000-2006 The Apache Software Foundation. All rights


+ 22
- 0
src/etc/testcases/taskdefs/style/build.xml View File

@@ -107,4 +107,26 @@
</copy>
</target>

<target name="testWithStyleAttrAndResource">
<property name="value" value="myvalue"/>
<xslt in="data.xml" out="${out.dir}/out.xml" style="printParams.xsl">
<file file="printParams.xsl"/>
</xslt>
</target>

<target name="testWithFileResource">
<xslt in="data.xml" out="${out.dir}/out.xml">
<file file="printParams.xsl"/>
<param name="set" expression="value"/>
</xslt>
</target>

<target name="testWithUrlResource">
<makeurl file="printParams.xsl" property="printParams.xsl.url"/>
<xslt in="data.xml" out="${out.dir}/out.xml">
<url url="${printParams.xsl.url}"/>
<param name="set" expression="value"/>
</xslt>
</target>
</project>

+ 34
- 0
src/main/org/apache/tools/ant/taskdefs/XSLTLiaison3.java View File

@@ -0,0 +1,34 @@
/*
* Copyright 2006 The Apache Software Foundation
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
*/
package org.apache.tools.ant.taskdefs;
import org.apache.tools.ant.types.Resource;
/**
* Extends Proxy interface for XSLT processors.
*
* @see XSLTProcess
* @since Ant 1.7
*/
public interface XSLTLiaison3 extends XSLTLiaison2 {
/**
* sets the stylesheet to use as a resource
* @param stylesheet the stylesheet to use as a resource
* @throws Exception if the stylesheet cannot be loaded
*/
void setStylesheet(Resource stylesheet) throws Exception;
}

+ 137
- 73
src/main/org/apache/tools/ant/taskdefs/XSLTProcess.java View File

@@ -54,9 +54,12 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
/** where to find the source XML file, default is the project's basedir */
private File baseDir = null;

/** XSL stylesheet */
/** XSL stylesheet as a filename */
private String xslFile = null;

/** XSL stylesheet as a {@link org.apache.tools.ant.types.Resource} */
private Resource xslResource = null;

/** extension of the files produced by XSL processing */
private String targetExtension = ".html";

@@ -217,7 +220,17 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
* @since Ant 1.7
*/
public void add(ResourceCollection rc) {
resources.add(rc);
resources.add(rc);
}

/**
* Adds the XSLT stylesheet as a resource
* @param xslResource the stylesheet as a
* {@link org.apache.tools.ant.types.Resource}
* @since Ant 1.7
*/
public void addConfigured(Resource xslResource) {
this.xslResource = xslResource;
}

/**
@@ -231,7 +244,7 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
mapper.add(fileNameMapper);
addMapper(mapper);
}
/**
* Executes the task.
*
@@ -240,7 +253,8 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
*/
public void execute() throws BuildException {
if ("style".equals(getTaskType())) {
log("Warning: the task name <style> is deprecated. Use <xslt> instead.", Project.MSG_WARN);
log("Warning: the task name <style> is deprecated. Use <xslt> instead.",
Project.MSG_WARN);
}

File savedBaseDir = baseDir;
@@ -249,8 +263,17 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
String[] list;
String[] dirs;

if (xslFile == null) {
throw new BuildException("no stylesheet specified", getLocation());
if (xslResource == null && xslFile == null) {
throw new BuildException("specify the "
+ "stylesheet either as a filename in style "
+ "attribute or as a nested resource", getLocation());

}
if (xslResource != null && xslFile != null) {
throw new BuildException("specify the "
+ "stylesheet either as a filename in style "
+ "attribute or as a nested resource but not "
+ "as both", getLocation());
}

if (inFile != null && !inFile.exists()) {
@@ -272,23 +295,31 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {

log("Using " + liaison.getClass().toString(), Project.MSG_VERBOSE);

File stylesheet = getProject().resolveFile(xslFile);
if (!stylesheet.exists()) {
stylesheet = FILE_UTILS.resolveFile(baseDir, xslFile);
/*
* shouldn't throw out deprecation warnings before we know,
* the wrong version has been used.
*/
if (stylesheet.exists()) {
log("DEPRECATED - the 'style' attribute should be relative "
+ "to the project's");
log(" basedir, not the tasks's basedir.");
if (xslFile != null) {
// If we enter here, it means that the stylesheet is supplied
// via style attribute
File stylesheet = FILE_UTILS.resolveFile(getProject().getBaseDir(), xslFile);
if (!stylesheet.exists()) {
stylesheet = FILE_UTILS.resolveFile(baseDir, xslFile);
/*
* shouldn't throw out deprecation warnings before we know,
* the wrong version has been used.
*/
if (stylesheet.exists()) {
log("DEPRECATED - the 'style' attribute should be relative "
+ "to the project's");
log(" basedir, not the tasks's basedir.");
}
}
FileResource fr = new FileResource();
fr.setProject(getProject());
fr.setFile(stylesheet);
xslResource = fr;
}

// if we have an in file and out then process them
if (inFile != null && outFile != null) {
process(inFile, outFile, stylesheet);
process(inFile, outFile, xslResource);
return;
}

@@ -298,34 +329,34 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
*/

//-- make sure destination directory exists...
checkDest();
checkDest();

if (useImplicitFileset) {
scanner = getDirectoryScanner(baseDir);
log("Transforming into " + destDir, Project.MSG_INFO);
if (useImplicitFileset) {
scanner = getDirectoryScanner(baseDir);
log("Transforming into " + destDir, Project.MSG_INFO);

// Process all the files marked for styling
list = scanner.getIncludedFiles();
for (int i = 0; i < list.length; ++i) {
process(baseDir, list[i], destDir, stylesheet);
}
if (performDirectoryScan) {
// Process all the directories marked for styling
dirs = scanner.getIncludedDirectories();
for (int j = 0; j < dirs.length; ++j) {
list = new File(baseDir, dirs[j]).list();
for (int i = 0; i < list.length; ++i) {
process(baseDir, dirs[j] + File.separator + list[i],
destDir, stylesheet);
// Process all the files marked for styling
list = scanner.getIncludedFiles();
for (int i = 0; i < list.length; ++i) {
process(baseDir, list[i], destDir, xslResource);
}
if (performDirectoryScan) {
// Process all the directories marked for styling
dirs = scanner.getIncludedDirectories();
for (int j = 0; j < dirs.length; ++j) {
list = new File(baseDir, dirs[j]).list();
for (int i = 0; i < list.length; ++i) {
process(baseDir, dirs[j] + File.separator + list[i],
destDir, xslResource);
}
}
}
} else { // only resource collections, there better be some
if (resources.size() == 0) {
throw new BuildException("no resources specified");
}
}
} else { // only resource collections, there better be some
if (resources.size() == 0) {
throw new BuildException("no resources specified");
}
}
processResources(stylesheet);
processResources(xslResource);
} finally {
if (loader != null) {
loader.resetThreadContextLoader();
@@ -337,7 +368,7 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
baseDir = savedBaseDir;
}
}
/**
* Set whether to check dependencies, or always generate;
* optional, default is false.
@@ -434,11 +465,11 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
*
* <p>Set this to false if you want explicit control with nested
* resource collections.</p>
*
* @param useimplicitfileset set to true if you want to use implicit fileset
* @since Ant 1.7
*/
public void setUseImplicitFileset(boolean b) {
useImplicitFileset = b;
public void setUseImplicitFileset(boolean useimplicitfileset) {
useImplicitFileset = useimplicitfileset;
}

/**
@@ -461,7 +492,7 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
private void resolveProcessor(String proc) throws Exception {
String classname;
if (proc.equals(PROCESSOR_TRAX)) {
classname= TRAX_LIAISON_CLASS;
classname = TRAX_LIAISON_CLASS;
} else if (proc.equals(PROCESSOR_XALAN1)) {
log("DEPRECATED - xalan processor is deprecated. Use trax "
+ "instead.");
@@ -531,24 +562,24 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
*
* @since Ant 1.7
*/
private void processResources(File stylesheet) {
Iterator iter = resources.iterator();
while (iter.hasNext()) {
Resource r = (Resource) iter.next();
if (!r.isExists()) {
continue;
}
File base = baseDir;
String name = r.getName();
if (r instanceof FileResource) {
FileResource f = (FileResource) r;
base = f.getBaseDir();
if (base == null) {
name = f.getFile().getAbsolutePath();
}
}
process(base, name, destDir, stylesheet);
}
private void processResources(Resource stylesheet) {
Iterator iter = resources.iterator();
while (iter.hasNext()) {
Resource r = (Resource) iter.next();
if (!r.isExists()) {
continue;
}
File base = baseDir;
String name = r.getName();
if (r instanceof FileResource) {
FileResource f = (FileResource) r;
base = f.getBaseDir();
if (base == null) {
name = f.getFile().getAbsolutePath();
}
}
process(base, name, destDir, stylesheet);
}
}

/**
@@ -562,14 +593,14 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
* @exception BuildException if the processing fails.
*/
private void process(File baseDir, String xmlFile, File destDir,
File stylesheet)
Resource stylesheet)
throws BuildException {

File outF = null;
File inF = null;

try {
long styleSheetLastModified = stylesheet.lastModified();
long styleSheetLastModified = stylesheet.getLastModified();
inF = new File(baseDir, xmlFile);

if (inF.isDirectory()) {
@@ -628,10 +659,10 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
* @param stylesheet the stylesheet to use.
* @exception BuildException if the processing fails.
*/
private void process(File inFile, File outFile, File stylesheet)
private void process(File inFile, File outFile, Resource stylesheet)
throws BuildException {
try {
long styleSheetLastModified = stylesheet.lastModified();
long styleSheetLastModified = stylesheet.getLastModified();
log("In file " + inFile + " time: " + inFile.lastModified(),
Project.MSG_DEBUG);
log("Out file " + outFile + " time: " + outFile.lastModified(),
@@ -917,10 +948,24 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {
/**
* Loads the stylesheet and set xsl:param parameters.
*
* @param stylesheet the file form which to load the stylesheet.
* @param stylesheet the file from which to load the stylesheet.
* @exception BuildException if the stylesheet cannot be loaded.
* @deprecated since Ant 1.7
*/
protected void configureLiaison(File stylesheet) throws BuildException {
FileResource fr = new FileResource();
fr.setProject(getProject());
fr.setFile(stylesheet);
configureLiaison(fr);
}
/**
* Loads the stylesheet and set xsl:param parameters.
*
* @param stylesheet the resource from which to load the stylesheet.
* @exception BuildException if the stylesheet cannot be loaded.
* @since Ant 1.7
*/
protected void configureLiaison(Resource stylesheet) throws BuildException {
if (stylesheetLoaded && reuseLoadedStylesheet) {
return;
}
@@ -928,16 +973,35 @@ public class XSLTProcess extends MatchingTask implements XSLTLogger {

try {
log("Loading stylesheet " + stylesheet, Project.MSG_INFO);
liaison.setStylesheet(stylesheet);
// We call liason.configure() and then liaison.setStylesheet()
// so that the internal variables of liaison can be set up
if (liaison instanceof XSLTLiaison2) {
((XSLTLiaison2) liaison).configure(this);
}

if (liaison instanceof XSLTLiaison3) {
// If we are here we can set the stylesheet as a
// resource
((XSLTLiaison3) liaison).setStylesheet(stylesheet);
} else {
// If we are here we cannot set the stylesheet as
// a resource, but we can set it as a file. So,
// we make an attempt to get it as a file
if (stylesheet instanceof FileResource) {
liaison.setStylesheet(
((FileResource) stylesheet).getFile());
} else {
throw new BuildException(liaison.getClass().toString()
+ " accepts the stylesheet only as a file",
getLocation());
}
}
for (Enumeration e = params.elements(); e.hasMoreElements();) {
Param p = (Param) e.nextElement();
if (p.shouldUse()) {
liaison.addParam(p.getName(), p.getExpression());
}
}
if (liaison instanceof XSLTLiaison2) {
((XSLTLiaison2) liaison).configure(this);
}
} catch (Exception ex) {
log("Failed to transform using stylesheet " + stylesheet,
Project.MSG_INFO);


+ 57
- 8
src/main/org/apache/tools/ant/taskdefs/optional/TraXLiaison.java View File

@@ -42,11 +42,14 @@ import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.TransformerConfigurationException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.XSLTLiaison2;
import org.apache.tools.ant.taskdefs.XSLTProcess;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.XSLTLiaison3;
import org.apache.tools.ant.taskdefs.XSLTLogger;
import org.apache.tools.ant.taskdefs.XSLTLoggerAware;
import org.apache.tools.ant.taskdefs.XSLTProcess;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.JAXPUtils;
import org.xml.sax.EntityResolver;
@@ -59,7 +62,12 @@ import org.xml.sax.XMLReader;
*
* @since Ant 1.3
*/
public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware {
public class TraXLiaison implements XSLTLiaison3, ErrorListener, XSLTLoggerAware {

/**
* The current <code>Project</code>
*/
private Project project;

/**
* the name of the factory implementation class to use
@@ -71,7 +79,7 @@ public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware
private TransformerFactory tfactory = null;

/** stylesheet to use for transformation */
private File stylesheet;
private Resource stylesheet;

private XSLTLogger logger;

@@ -115,13 +123,24 @@ public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware
* @throws Exception on error
*/
public void setStylesheet(File stylesheet) throws Exception {
FileResource fr = new FileResource();
fr.setProject(project);
fr.setFile(stylesheet);
}

/**
* Set the stylesheet file.
* @param stylesheet a {@link org.apache.tools.ant.types.Resource} value
* @throws Exception on error
*/
public void setStylesheet(Resource stylesheet) throws Exception {
if (this.stylesheet != null) {
// resetting the stylesheet - reset transformer
transformer = null;

// do we need to reset templates as well
if (!this.stylesheet.equals(stylesheet)
|| (stylesheet.lastModified() != templatesModTime)) {
|| (stylesheet.getLastModified() != templatesModTime)) {
templates = null;
}
}
@@ -205,6 +224,35 @@ public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware
return src;
}

private Source getSource(InputStream is, Resource resource)
throws ParserConfigurationException, SAXException {
// todo: is this comment still relevant ??
// FIXME: need to use a SAXSource as the source for the transform
// so we can plug in our own entity resolver
Source src = null;
if (entityResolver != null) {
if (getFactory().getFeature(SAXSource.FEATURE)) {
SAXParserFactory spFactory = SAXParserFactory.newInstance();
spFactory.setNamespaceAware(true);
XMLReader reader = spFactory.newSAXParser().getXMLReader();
reader.setEntityResolver(entityResolver);
src = new SAXSource(reader, new InputSource(is));
} else {
throw new IllegalStateException("xcatalog specified, but "
+ "parser doesn't support SAX");
}
} else {
// WARN: Don't use the StreamSource(File) ctor. It won't work with
// xalan prior to 2.2 because of systemid bugs.
src = new StreamSource(is);
}
// The line below is a hack: the system id must an URI, but it is not
// cleat to get the URI of an resource, so just set the name of the
// resource as a system id
src.setSystemId(resource.getName());
return src;
}

/**
* Read in templates from the stylesheet
*/
@@ -219,8 +267,8 @@ public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware
InputStream xslStream = null;
try {
xslStream
= new BufferedInputStream(new FileInputStream(stylesheet));
templatesModTime = stylesheet.lastModified();
= new BufferedInputStream(stylesheet.getInputStream());
templatesModTime = stylesheet.getLastModified();
Source src = getSource(xslStream, stylesheet);
templates = getFactory().newTemplates(src);
} finally {
@@ -437,7 +485,7 @@ public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware
/**
* @param file the filename to use for the systemid
* @return the systemid
* @deprecated since 1.5.x.
* @deprecated since 1.5.x.
* Use org.apache.tools.ant.util.JAXPUtils#getSystemId instead.
*/
protected String getSystemId(File file) {
@@ -451,6 +499,7 @@ public class TraXLiaison implements XSLTLiaison2, ErrorListener, XSLTLoggerAware
* is to be configured.
*/
public void configure(XSLTProcess xsltTask) {
project = xsltTask.getProject();
XSLTProcess.Factory factory = xsltTask.getFactory();
if (factory != null) {
setFactory(factory.getName());


+ 32
- 6
src/testcases/org/apache/tools/ant/taskdefs/StyleTest.java View File

@@ -33,6 +33,8 @@ import org.apache.tools.ant.util.FileUtils;
*/
public class StyleTest extends BuildFileTest {

private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();

public StyleTest(String s) {
super(s);
}
@@ -48,7 +50,10 @@ public class StyleTest extends BuildFileTest {
}

public void testStyleIsSet() throws Exception {
expectBuildException("testStyleIsSet", "no stylesheet specified");
expectSpecificBuildException("testStyleIsSet",
"no stylesheet specified", "specify the " +
"stylesheet either as a filename in style " +
"attribute or as a nested resource");
}

public void testTransferParameterSet() throws Exception {
@@ -91,21 +96,24 @@ public class StyleTest extends BuildFileTest {
}

public void testDefaultMapper(String target) throws Exception {
assertTrue(!getProject().resolveFile("out/data.html").exists());
assertTrue(!(FileUtils.getFileUtils().resolveFile(
getProject().getBaseDir(),"out/data.html")).exists());
expectFileContains(target,
"out/data.html",
"set='myvalue'");
}

public void testCustomMapper() throws Exception {
assertTrue(!getProject().resolveFile("out/out.xml").exists());
assertTrue(!FILE_UTILS.resolveFile(
getProject().getBaseDir(), "out/out.xml").exists());
expectFileContains("testCustomMapper",
"out/out.xml",
"set='myvalue'");
}

public void testTypedMapper() throws Exception {
assertTrue(!getProject().resolveFile("out/out.xml").exists());
assertTrue(!FILE_UTILS.resolveFile(
getProject().getBaseDir(), "out/out.xml").exists());
expectFileContains("testTypedMapper",
"out/out.xml",
"set='myvalue'");
@@ -113,16 +121,34 @@ public class StyleTest extends BuildFileTest {

public void testDirectoryHierarchyWithDirMatching() throws Exception {
executeTarget("testDirectoryHierarchyWithDirMatching");
assertTrue(getProject().resolveFile("out/dest/level1/data.html")
assertTrue(FILE_UTILS.resolveFile(
getProject().getBaseDir(), "out/dest/level1/data.html")
.exists());
}

public void testDirsWithSpaces() throws Exception {
executeTarget("testDirsWithSpaces");
assertTrue(getProject().resolveFile("out/d est/data.html")
assertTrue(FILE_UTILS.resolveFile(
getProject().getBaseDir(), "out/d est/data.html")
.exists());
}

public void testWithStyleAttrAndResource() throws Exception {
expectSpecificBuildException("testWithStyleAttrAndResource",
"Must throws a BuildException", "specify the " +
"stylesheet either as a filename in style " +
"attribute or as a nested resource but not " +
"as both");
}

public void testWithFileResource() throws Exception {
expectFileContains("testWithFileResource", "out/out.xml", "set='value'");
}

public void testWithUrlResource() throws Exception {
expectFileContains("testWithUrlResource", "out/out.xml", "set='value'");
}

// ************* copied from ConcatTest *************

// ------------------------------------------------------


Loading…
Cancel
Save