Copy directory from a jar file
Asked Answered
B

13

39

I have recently developed an application and created the jar file.

One of my classes creates an output directory, populating it with files from its resource.

My code is something like this:

// Copy files from dir "template" in this class resource to output.
private void createOutput(File output) throws IOException {

    File template = new File(FileHelper.URL2Path(getClass().getResource("template")));
    FileHelper.copyDirectory(template, output);
}

Unfortunately this doesn't work.

I tried the following without luck:

While writing this I was thinking about instead of having a template dir in the resource path having a zip file of it. Doing it this way I could get the file as an inputStream and unzip it where I need to. But I am not sure if it's the correct way.

Belorussia answered 6/9, 2009 at 21:46 Comment(1)
please reset the correct answer, thxFehr
R
15

I think your approach of using a zip file makes sense. Presumably you'll do a getResourceAsStream to get at the internals of the zip, which will logically look like a directory tree.

A skeleton approach:

InputStream is = getClass().getResourceAsStream("my_embedded_file.zip");
ZipInputStream zis = new ZipInputStream(is);
ZipEntry entry;

while ((entry = zis.getNextEntry()) != null) {
    // do something with the entry - for example, extract the data 
}
Relation answered 6/9, 2009 at 22:11 Comment(1)
I end up doing this. Zipping everything and getting the zip as resource afterwards to unzip it where needed.Belorussia
F
19

Thanks for the solution! For others, the following doesn't make use of the auxiliary classes (except for StringUtils)

/I added extra information for this solution, check the end of the code, Zegor V/

public class FileUtils {
  public static boolean copyFile(final File toCopy, final File destFile) {
    try {
      return FileUtils.copyStream(new FileInputStream(toCopy),
          new FileOutputStream(destFile));
    } catch (final FileNotFoundException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean copyFilesRecusively(final File toCopy,
      final File destDir) {
    assert destDir.isDirectory();

    if (!toCopy.isDirectory()) {
      return FileUtils.copyFile(toCopy, new File(destDir, toCopy.getName()));
    } else {
      final File newDestDir = new File(destDir, toCopy.getName());
      if (!newDestDir.exists() && !newDestDir.mkdir()) {
        return false;
      }
      for (final File child : toCopy.listFiles()) {
        if (!FileUtils.copyFilesRecusively(child, newDestDir)) {
          return false;
        }
      }
    }
    return true;
  }

  public static boolean copyJarResourcesRecursively(final File destDir,
      final JarURLConnection jarConnection) throws IOException {

    final JarFile jarFile = jarConnection.getJarFile();

    for (final Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
      final JarEntry entry = e.nextElement();
      if (entry.getName().startsWith(jarConnection.getEntryName())) {
        final String filename = StringUtils.removeStart(entry.getName(), //
            jarConnection.getEntryName());

        final File f = new File(destDir, filename);
        if (!entry.isDirectory()) {
          final InputStream entryInputStream = jarFile.getInputStream(entry);
          if(!FileUtils.copyStream(entryInputStream, f)){
            return false;
          }
          entryInputStream.close();
        } else {
          if (!FileUtils.ensureDirectoryExists(f)) {
            throw new IOException("Could not create directory: "
                + f.getAbsolutePath());
          }
        }
      }
    }
    return true;
  }

  public static boolean copyResourcesRecursively( //
      final URL originUrl, final File destination) {
    try {
      final URLConnection urlConnection = originUrl.openConnection();
      if (urlConnection instanceof JarURLConnection) {
        return FileUtils.copyJarResourcesRecursively(destination,
            (JarURLConnection) urlConnection);
      } else {
        return FileUtils.copyFilesRecusively(new File(originUrl.getPath()),
            destination);
      }
    } catch (final IOException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean copyStream(final InputStream is, final File f) {
    try {
      return FileUtils.copyStream(is, new FileOutputStream(f));
    } catch (final FileNotFoundException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean copyStream(final InputStream is, final OutputStream os) {
    try {
      final byte[] buf = new byte[1024];

      int len = 0;
      while ((len = is.read(buf)) > 0) {
        os.write(buf, 0, len);
      }
      is.close();
      os.close();
      return true;
    } catch (final IOException e) {
      e.printStackTrace();
    }
    return false;
  }

  private static boolean ensureDirectoryExists(final File f) {
    return f.exists() || f.mkdir();
  }
}

It uses only one external library from the Apache Software Foundation, however the used functions are only :

  public static String removeStart(String str, String remove) {
      if (isEmpty(str) || isEmpty(remove)) {
          return str;
      }
      if (str.startsWith(remove)){
          return str.substring(remove.length());
      }
      return str;
  }
  public static boolean isEmpty(CharSequence cs) {
      return cs == null || cs.length() == 0;
  }

My knowledge is limited on Apache licence, but you can use this methods in your code without library. However, i am not responsible for licence issues, if there is.

Frazzled answered 27/7, 2010 at 21:27 Comment(1)
I found this immensely helpful after asking this question: #17929222Feme
L
18

Using Java7+ this can be achieved by creating FileSystem and then using walkFileTree to copy files recursively.

public void copyFromJar(String source, final Path target) throws URISyntaxException, IOException {
    URI resource = getClass().getResource("").toURI();
    FileSystem fileSystem = FileSystems.newFileSystem(
            resource,
            Collections.<String, String>emptyMap()
    );


    final Path jarPath = fileSystem.getPath(source);

    Files.walkFileTree(jarPath, new SimpleFileVisitor<Path>() {

        private Path currentTarget;

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            currentTarget = target.resolve(jarPath.relativize(dir).toString());
            Files.createDirectories(currentTarget);
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            Files.copy(file, target.resolve(jarPath.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING);
            return FileVisitResult.CONTINUE;
        }

    });
}

The method can be used like this:

copyFromJar("/path/to/the/template/in/jar", Paths.get("/tmp/from-jar"))
Lewendal answered 19/6, 2014 at 21:14 Comment(5)
Cool answer, Java 7 has lots of neat stuff. Seems like your utility should take 3 arguments: (1) virtual path within jar file to copy from, (2) destination directory in the filesystem, and (3) path to actual .jar file archive to extract from. I think your args #'s 1 and 2 match up with that I'm saying, but not sure on arg # 3? Maybe you're assuming it's the jar we're running from, or somehow letting the JVM class loader find the resource, presumably in one of the jars on the classpath? Just checking, and THANKS!Angelangela
This answer does not work. I'm intrigued to try and use the (relatively) new nio API, but I can't seem to make it work: java.lang.IllegalArgumentException: Path component should be '/'Sherikasherill
Good solution! I was able to copy a folder with all files from jar /resources without external libraries. Thank you!Birthroot
@Sherikasherill I got the error you described "Path component should be '/'" when the resource happens to be in the class path. The url resolves to a file url and not in a jar in that case. Try to modify this solution with the answer provided by Duc below.Inception
This is the simplest most concise answer. Definitely deserves to be #1. It's also using nio, which is probably faster, right?Calcar
R
15

I think your approach of using a zip file makes sense. Presumably you'll do a getResourceAsStream to get at the internals of the zip, which will logically look like a directory tree.

A skeleton approach:

InputStream is = getClass().getResourceAsStream("my_embedded_file.zip");
ZipInputStream zis = new ZipInputStream(is);
ZipEntry entry;

while ((entry = zis.getNextEntry()) != null) {
    // do something with the entry - for example, extract the data 
}
Relation answered 6/9, 2009 at 22:11 Comment(1)
I end up doing this. Zipping everything and getting the zip as resource afterwards to unzip it where needed.Belorussia
S
8

I hated the idea of using the ZIP file method posted earlier, so I came up with the following.

public void copyResourcesRecursively(URL originUrl, File destination) throws Exception {
    URLConnection urlConnection = originUrl.openConnection();
    if (urlConnection instanceof JarURLConnection) {
        copyJarResourcesRecursively(destination, (JarURLConnection) urlConnection);
    } else if (urlConnection instanceof FileURLConnection) {
        FileUtils.copyFilesRecursively(new File(originUrl.getPath()), destination);
    } else {
        throw new Exception("URLConnection[" + urlConnection.getClass().getSimpleName() +
                "] is not a recognized/implemented connection type.");
    }
}

public void copyJarResourcesRecursively(File destination, JarURLConnection jarConnection ) throws IOException {
    JarFile jarFile = jarConnection.getJarFile();
    for (JarEntry entry : CollectionUtils.iterable(jarFile.entries())) {
        if (entry.getName().startsWith(jarConnection.getEntryName())) {
            String fileName = StringUtils.removeStart(entry.getName(), jarConnection.getEntryName());
            if (!entry.isDirectory()) {
                InputStream entryInputStream = null;
                try {
                    entryInputStream = jarFile.getInputStream(entry);
                    FileUtils.copyStream(entryInputStream, new File(destination, fileName));
                } finally {
                    FileUtils.safeClose(entryInputStream);
                }
            } else {
                FileUtils.ensureDirectoryExists(new File(destination, fileName));
            }
        }
    }
}

Example Useage (copies all files from the classpath resource "config" to "${homeDirectory}/config":

File configHome = new File(homeDirectory, "config/");
//noinspection ResultOfMethodCallIgnored
configHome.mkdirs();
copyResourcesRecursively(super.getClass().getResource("/config"), configHome);

This should work both for copying from both flat files as well as Jar files.

Note: The code above uses some custom utility classes (FileUtils, CollectionUtils) as well as some from Apache commons-lang (StringUtils), but the functions should be named fairly obviously.

Stannite answered 7/6, 2010 at 23:36 Comment(1)
What libary are you using? (FileUtils, CollectionUtils, ...)Relator
P
6

The answer of lpiepiora, is correct! But there is a minor issue, The source, should be a jar Url. When the source path is path to a file system, then the above code will not work proper. To solve this problem, you should use the ReferencePath, the code, you can get from the following link: Read from file system via FileSystem object The new code of copyFromJar should like:

public class ResourcesUtils {
public static void copyFromJar(final String sourcePath, final Path target) throws URISyntaxException,
        IOException {
    final PathReference pathReference = PathReference.getPath(new URI(sourcePath));
    final Path jarPath = pathReference.getPath();

    Files.walkFileTree(jarPath, new SimpleFileVisitor<Path>() {

        private Path currentTarget;

        @Override
        public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
            currentTarget = target.resolve(jarPath.relativize(dir)
                    .toString());
            Files.createDirectories(currentTarget);
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
            Files.copy(file, target.resolve(jarPath.relativize(file)
                    .toString()), StandardCopyOption.REPLACE_EXISTING);
            return FileVisitResult.CONTINUE;
        }

    });
}

public static void main(final String[] args) throws MalformedURLException, URISyntaxException, IOException {
    final String sourcePath = "jar:file:/c:/temp/example.jar!/src/main/resources";
    ResourcesUtils.copyFromJar(sourcePath, Paths.get("c:/temp/resources"));
}
Peseta answered 2/10, 2015 at 8:15 Comment(1)
The link you provided does not contain a complete implementation of PathReference.Somnus
R
3

I'm not sure what FileHelper is or does, but you will NOT be able to copy files (or directories) directly from JAR. Using InputStream as you've mentioned is the correct way (from either jar or zip):

InputStream is = getClass().getResourceAsStream("file_in_jar");
OutputStream os = new FileOutputStream("dest_file");
byte[] buffer = new byte[4096];
int length;
while ((length = is.read(buffer)) > 0) {
    os.write(buffer, 0, length);
}
os.close();
is.close();

You'll need to do the above (handling exceptions appropriately, of course) for each of your files. You may or may not be able (depending on your deployment configuration) to read jar file in question as JarFile (it may not be available as an actual file if deployed as part of non-expanded web app, for example). If you can read it, you should be able to iterate through list of JarEntry instances and thus reconstitute your directory structure; otherwise you may need to store it elsewhere (within text or xml resource, for example)

You may want to take a look at Commons IO library - it provides a lot of commonly used stream / file functionality including copying.

Rachele answered 6/9, 2009 at 22:5 Comment(0)
F
3

Here is a working version from the tess4j project:

 /**
 * This method will copy resources from the jar file of the current thread and extract it to the destination folder.
 * 
 * @param jarConnection
 * @param destDir
 * @throws IOException
 */
public void copyJarResourceToFolder(JarURLConnection jarConnection, File destDir) {

    try {
        JarFile jarFile = jarConnection.getJarFile();

        /**
         * Iterate all entries in the jar file.
         */
        for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {

            JarEntry jarEntry = e.nextElement();
            String jarEntryName = jarEntry.getName();
            String jarConnectionEntryName = jarConnection.getEntryName();

            /**
             * Extract files only if they match the path.
             */
            if (jarEntryName.startsWith(jarConnectionEntryName)) {

                String filename = jarEntryName.startsWith(jarConnectionEntryName) ? jarEntryName.substring(jarConnectionEntryName.length()) : jarEntryName;
                File currentFile = new File(destDir, filename);

                if (jarEntry.isDirectory()) {
                    currentFile.mkdirs();
                } else {
                    InputStream is = jarFile.getInputStream(jarEntry);
                    OutputStream out = FileUtils.openOutputStream(currentFile);
                    IOUtils.copy(is, out);
                    is.close();
                    out.close();
                }
            }
        }
    } catch (IOException e) {
        // TODO add logger
        e.printStackTrace();
    }

}
Fehr answered 1/9, 2014 at 22:28 Comment(2)
Funny thing, I was looking for the way to copy tesseract trained data. Good thing they have it!Exorcist
Yes, you're right, I've added it there back then.Fehr
V
3

I know this question is kind of old now but after trying some answers that didn't work and others that required a whole library for just one method, I decided to put together a class. It doesn't require third-party libraries and it's been tested with Java 8. There are four public methods: copyResourcesToTempDir, copyResourcesToDir, copyResourceDirectory and jar.

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.util.Enumeration;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * A helper to copy resources from a JAR file into a directory.
 */
public final class ResourceCopy {

    /**
     * URI prefix for JAR files.
     */
    private static final String JAR_URI_PREFIX = "jar:file:";

    /**
     * The default buffer size.
     */
    private static final int BUFFER_SIZE = 8 * 1024;

    /**
     * Copies a set of resources into a temporal directory, optionally preserving
     * the paths of the resources.
     * @param preserve Whether the files should be placed directly in the
     *  directory or the source path should be kept
     * @param paths The paths to the resources
     * @return The temporal directory
     * @throws IOException If there is an I/O error
     */
    public File copyResourcesToTempDir(final boolean preserve,
        final String... paths)
        throws IOException {
        final File parent = new File(System.getProperty("java.io.tmpdir"));
        File directory;
        do {
            directory = new File(parent, String.valueOf(System.nanoTime()));
        } while (!directory.mkdir());
        return this.copyResourcesToDir(directory, preserve, paths);
    }

    /**
     * Copies a set of resources into a directory, preserving the paths
     * and names of the resources.
     * @param directory The target directory
     * @param preserve Whether the files should be placed directly in the
     *  directory or the source path should be kept
     * @param paths The paths to the resources
     * @return The temporal directory
     * @throws IOException If there is an I/O error
     */
    public File copyResourcesToDir(final File directory, final boolean preserve,
        final String... paths) throws IOException {
        for (final String path : paths) {
            final File target;
            if (preserve) {
                target = new File(directory, path);
                target.getParentFile().mkdirs();
            } else {
                target = new File(directory, new File(path).getName());
            }
            this.writeToFile(
                Thread.currentThread()
                    .getContextClassLoader()
                    .getResourceAsStream(path),
                target
            );
        }
        return directory;
    }

    /**
     * Copies a resource directory from inside a JAR file to a target directory.
     * @param source The JAR file
     * @param path The path to the directory inside the JAR file
     * @param target The target directory
     * @throws IOException If there is an I/O error
     */
    public void copyResourceDirectory(final JarFile source, final String path,
        final File target) throws IOException {
        final Enumeration<JarEntry> entries = source.entries();
        final String newpath = String.format("%s/", path);
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            if (entry.getName().startsWith(newpath) && !entry.isDirectory()) {
                final File dest =
                    new File(target, entry.getName().substring(newpath.length()));
                final File parent = dest.getParentFile();
                if (parent != null) {
                    parent.mkdirs();
                }
                this.writeToFile(source.getInputStream(entry), dest);
            }
        }
    }

    /**
     * The JAR file containing the given class.
     * @param clazz The class
     * @return The JAR file or null
     * @throws IOException If there is an I/O error
     */
    public Optional<JarFile> jar(final Class<?> clazz) throws IOException {
        final String path =
            String.format("/%s.class", clazz.getName().replace('.', '/'));
        final URL url = clazz.getResource(path);
        Optional<JarFile> optional = Optional.empty();
        if (url != null) {
            final String jar = url.toString();
            final int bang = jar.indexOf('!');
            if (jar.startsWith(ResourceCopy.JAR_URI_PREFIX) && bang != -1) {
                optional = Optional.of(
                    new JarFile(
                        jar.substring(ResourceCopy.JAR_URI_PREFIX.length(), bang)
                    )
                );
            }
        }
        return optional;
    }

    /**
     * Writes an input stream to a file.
     * @param input The input stream
     * @param target The target file
     * @throws IOException If there is an I/O error
     */
    private void writeToFile(final InputStream input, final File target)
        throws IOException {
        final OutputStream output = Files.newOutputStream(target.toPath());
        final byte[] buffer = new byte[ResourceCopy.BUFFER_SIZE];
        int length = input.read(buffer);
        while (length > 0) {
            output.write(buffer, 0, length);
            length = input.read(buffer);
        }
        input.close();
        output.close();
    }

}
Vermiculite answered 10/10, 2019 at 7:54 Comment(3)
Your answer should note that it will work if and only if the resources are in a jar file. Class loaders may load resources from other things than a jar file.Bioclimatology
@Bioclimatology Thank you for the clarification. Your contributions are welcomed.Moral
This piece of code saved my day ; and brought my work into a wonderful multiplatform compatible JAR with JNA. upvoted. Another +1 for it being a proper class.Thynne
L
2

You could use the ClassLoader to obtain a stream to the resource. Once you have obtained an InputStream, you can read off, and write the contents of the stream, onto an OutputStream.

In your case, you'll need to create several OutputStream instances, one for each file that you want to copy over to the destination. This of course, requires that you know of the file names before hand.

For this task, it is preferred to use getResourceAsStream, rather than getResource or getResources().

Layoff answered 6/9, 2009 at 22:1 Comment(4)
Vineet: I don't like the "requires that you know of the file names before hand.". I should have some sort of Static list with every file in the template dir and update it when it changes it. :(Belorussia
Yes, unfortunately one cannot get a directory listing inside JARs. You can refer the SO question #909500Layoff
And there's another one @#133729 . If you read carefully, you'll notice that the OP did think of maintaining a static list of files to read.Layoff
If this doesn't work out, the other answer provided - reading from a ZIP stream etc. will definitely work.Layoff
F
2

I have faced the similair problem recently. I tried to extract folder from java resources. So I resolved this issue with Spring PathMatchingResourcePatternResolver.

This code gets all files and directories from the specified resource:

        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                + resourceFolder + "/**");

And this is the class that copy all files and directories from the resource to the disk path.

public class ResourceExtractor {

public static final Logger logger = 
Logger.getLogger(ResourceExtractor.class);

public void extract(String resourceFolder, String destinationFolder){
    try {
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                + resourceFolder + "/**");
        URI inJarUri  = new DefaultResourceLoader().getResource("classpath:" + resourceFolder).getURI();

        for (Resource resource : resources){
            String relativePath = resource
                        .getURI()
                        .getRawSchemeSpecificPart()
                        .replace(inJarUri.getRawSchemeSpecificPart(), "");
            if (relativePath.isEmpty()){
                continue;
            }
            if (relativePath.endsWith("/") || relativePath.endsWith("\\")) {
                File dirFile = new File(destinationFolder + relativePath);
                if (!dirFile.exists()) {
                    dirFile.mkdir();
                }
            }
            else{
                copyResourceToFilePath(resource, destinationFolder + relativePath);
            }
        }
    }
    catch (IOException e){
        logger.debug("Extraction failed!", e );
    }
}

private void copyResourceToFilePath(Resource resource, String filePath) throws IOException{
    InputStream resourceInputStream = resource.getInputStream();
    File file = new File(filePath);
    if (!file.exists()) {
        FileUtils.copyInputStreamToFile(resourceInputStream, file);
    }
}

}

Fantasia answered 1/8, 2017 at 7:46 Comment(2)
Thanks a lot for sharing. I have used your class in a prototypical command-line application based on Spring Shell to extract some Thymeleaf templates from the Jar. That worked really well!Oldcastle
good solution in spring boot contextSevigny
T
1

I liked the response from @nivekastoreth (https://mcmap.net/q/400634/-copy-directory-from-a-jar-file) but was not able to use it directly because it was relying on third party libraries.

I just rewrote it to use java.nio.file.Files

Let's say I want to copy a folder like this:

parent
|_folder
  |_item1
  |_item2
|_folder2

I can just call the method like this:

final URL configFolderURL = getClass().getResource("in/jar/path/folder");
Path targetDir = Files.createDirectory(Path.of("a","new","dir"));
copyJarResourcesRecursively(targetDir, (JarURLConnection) configFolderURL.openConnection());

Here is the implementation:

private void copyJarResourcesRecursively(Path destination, JarURLConnection jarConnection) throws IOException {
        JarFile jarFile = jarConnection.getJarFile();
        for (Iterator<JarEntry> it = jarFile.entries().asIterator(); it.hasNext();) {
            JarEntry entry = it.next();
            if (entry.getName().startsWith(jarConnection.getEntryName())) {
                if (!entry.isDirectory()) {
                    try (InputStream entryInputStream = jarFile.getInputStream(entry)) {
                        Files.copy(entryInputStream, Paths.get(destination.toString(), entry.getName()));
                    }
                } else {
                    Files.createDirectories(Paths.get(destination.toString(), entry.getName()));
                }
            }
        }
    }
Tiddly answered 16/5, 2022 at 17:6 Comment(0)
E
1

It can very easy using (apache commons-io) dependency

import org.apache.commons.io.FileUtils

public void copyResourceFileInsideJarToPath(String resourceName, Path toPath) throws IOException {
    URL fromURL = getClass().getClassLoader().getResource(resourceName);
    LOGGER.info("fromURL: {}", fromURL);
    LOGGER.info("toPath: {}", toPath.toAbsolutePath());

    Files.deleteIfExists(toPath);
    FileUtils.copyURLToFile(fromURL, toPath.toFile());
}

Related maven dependency:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>
Emmen answered 26/1, 2023 at 9:48 Comment(0)
G
0

You can use my library: compile group: 'com.github.ardenliu', name: 'arden-file', version: '0.0.4'

ResourcesUtils class: copyFromClassPath(final String resourcePath, final Path targetRoot)

Source Code: https://github.com/ardenliu/common/blob/master/arden-file/src/main/java/com/github/ardenliu/common/file/ResourcesUtils.java

Junit testing: One testing case for Eclipse classpath; another one for jar https://github.com/ardenliu/common/blob/master/arden-file/src/test/java/com/github/ardenliu/common/file/ResourcesUtilsTest.java

Ghibelline answered 16/5, 2020 at 2:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.