รับรายการทรัพยากรจากไดเรกทอรี classpath


247

ฉันกำลังมองหาวิธีการที่จะได้รับรายชื่อทรัพยากรทั้งหมดจากไดเรกทอรี classpath List<String> getResourceNames (String directoryName)ที่กำหนดสิ่งที่ต้องการวิธีการที่

ตัวอย่างเช่นกำหนด classpath ไดเรกทอรีx/y/zที่มีไฟล์a.html, b.html, c.htmlและไดเรกทอรีย่อยd,getResourceNames("x/y/z")ควรกลับมีสายต่อไปนี้:List<String>['a.html', 'b.html', 'c.html', 'd']

มันควรทำงานทั้งสองอย่างสำหรับทรัพยากรในระบบไฟล์และไห

ฉันรู้ว่าฉันสามารถเขียนตัวอย่างสั้น ๆ ด้วยFiles, JarFiles และURLs แต่ฉันไม่ต้องการที่จะบูรณาการล้อ คำถามของฉันคือเมื่อมีห้องสมุดสาธารณะที่มีอยู่แล้ววิธีที่เร็วที่สุดที่จะใช้getResourceNamesคืออะไร กองสปริงและ Apache Commons สามารถทำได้ทั้งคู่


1
คำตอบที่เกี่ยวข้อง: stackoverflow.com/a/30149061/4102160
Cfx

ตรวจสอบโครงการนี้แก้ไขการสแกนโฟลเดอร์ทรัพยากร: github.com/fraballi/resources-folder-scanner
Felix Aballi

คำตอบ:


165

สแกนเนอร์ที่กำหนดเอง

ใช้เครื่องสแกนของคุณเอง ตัวอย่างเช่น:

private List<String> getResourceFiles(String path) throws IOException {
    List<String> filenames = new ArrayList<>();

    try (
            InputStream in = getResourceAsStream(path);
            BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
        String resource;

        while ((resource = br.readLine()) != null) {
            filenames.add(resource);
        }
    }

    return filenames;
}

private InputStream getResourceAsStream(String resource) {
    final InputStream in
            = getContextClassLoader().getResourceAsStream(resource);

    return in == null ? getClass().getResourceAsStream(resource) : in;
}

private ClassLoader getContextClassLoader() {
    return Thread.currentThread().getContextClassLoader();
}

Spring Framework

ใช้PathMatchingResourcePatternResolverจาก Spring Framework

Ronmamo Reflections

เทคนิคอื่น ๆ อาจช้าที่รันไทม์สำหรับค่า CLASSPATH ขนาดใหญ่ ทางออกที่เร็วกว่าคือการใช้Reflections APIของ ronmamo ซึ่งทำการคอมไพล์การค้นหาในเวลารวบรวม


12
ถ้าใช้ Reflections สิ่งที่คุณต้องการจริง ๆ :new Reflections("my.package", new ResourcesScanner()).getResources(pattern)
zapp

27
โซลูชันแรกใช้งานได้เมื่อเรียกใช้จากไฟล์ jar หรือไม่ สำหรับฉัน - มันไม่ ฉันสามารถอ่านไฟล์ด้วยวิธีนี้ แต่อ่านไดเรกทอรีไม่ได้
Valentina Chumak

5
วิธีแรกที่อธิบายไว้ในคำตอบนี้ - การอ่านเส้นทางเป็นทรัพยากรดูเหมือนจะไม่ทำงานเมื่อทรัพยากรอยู่ใน JAR เดียวกับรหัสที่ปฏิบัติการได้อย่างน้อยกับ OpenJDK 1.8 ความล้มเหลวค่อนข้างคี่ - NullPointerException จะถูกโยนลงมาจากส่วนลึกในตรรกะการประมวลผลไฟล์ของ JVM ราวกับว่านักออกแบบไม่ได้คาดหวังการใช้ทรัพยากรนี้จริง ๆ และมีเพียงการนำไปใช้บางส่วนเท่านั้น แม้ว่ามันจะทำงานได้กับ JDKs หรือสภาพแวดล้อมบางอย่างมันก็ไม่ได้มีการบันทึกพฤติกรรม
Kevin Boone

7
คุณไม่สามารถอ่านไดเรกทอรีเช่นนี้ได้เนื่องจากไม่มีไดเรกทอรีใด ๆ แต่เป็นสตรีมไฟล์ คำตอบนี้ผิดครึ่ง
โทมัส Decaux

4
วิธีแรกไม่ได้ผล - อย่างน้อยตอนที่ทำงานจากสภาพแวดล้อมการพัฒนาก่อนที่จะทำให้ทรัพยากรเสียหาย การเรียกไปยังgetResourceAsStream()พา ธ ที่สัมพันธ์กับไดเรกทอรีจะนำไปสู่ ​​FileInputStream (ไฟล์) ซึ่งกำหนดให้โยน FileNotFoundException หากไฟล์นั้นเป็นไดเรกทอรี
Andy Thomas

52

นี่คือรหัส
ที่มา : forums.devx.com/showthread.php?t=153784

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

/**
 * list resources available from the classpath @ *
 */
public class ResourceList{

    /**
     * for all elements of java.class.path get a Collection of resources Pattern
     * pattern = Pattern.compile(".*"); gets all resources
     * 
     * @param pattern
     *            the pattern to match
     * @return the resources in the order they are found
     */
    public static Collection<String> getResources(
        final Pattern pattern){
        final ArrayList<String> retval = new ArrayList<String>();
        final String classPath = System.getProperty("java.class.path", ".");
        final String[] classPathElements = classPath.split(System.getProperty("path.separator"));
        for(final String element : classPathElements){
            retval.addAll(getResources(element, pattern));
        }
        return retval;
    }

    private static Collection<String> getResources(
        final String element,
        final Pattern pattern){
        final ArrayList<String> retval = new ArrayList<String>();
        final File file = new File(element);
        if(file.isDirectory()){
            retval.addAll(getResourcesFromDirectory(file, pattern));
        } else{
            retval.addAll(getResourcesFromJarFile(file, pattern));
        }
        return retval;
    }

    private static Collection<String> getResourcesFromJarFile(
        final File file,
        final Pattern pattern){
        final ArrayList<String> retval = new ArrayList<String>();
        ZipFile zf;
        try{
            zf = new ZipFile(file);
        } catch(final ZipException e){
            throw new Error(e);
        } catch(final IOException e){
            throw new Error(e);
        }
        final Enumeration e = zf.entries();
        while(e.hasMoreElements()){
            final ZipEntry ze = (ZipEntry) e.nextElement();
            final String fileName = ze.getName();
            final boolean accept = pattern.matcher(fileName).matches();
            if(accept){
                retval.add(fileName);
            }
        }
        try{
            zf.close();
        } catch(final IOException e1){
            throw new Error(e1);
        }
        return retval;
    }

    private static Collection<String> getResourcesFromDirectory(
        final File directory,
        final Pattern pattern){
        final ArrayList<String> retval = new ArrayList<String>();
        final File[] fileList = directory.listFiles();
        for(final File file : fileList){
            if(file.isDirectory()){
                retval.addAll(getResourcesFromDirectory(file, pattern));
            } else{
                try{
                    final String fileName = file.getCanonicalPath();
                    final boolean accept = pattern.matcher(fileName).matches();
                    if(accept){
                        retval.add(fileName);
                    }
                } catch(final IOException e){
                    throw new Error(e);
                }
            }
        }
        return retval;
    }

    /**
     * list the resources that match args[0]
     * 
     * @param args
     *            args[0] is the pattern to match, or list all resources if
     *            there are no args
     */
    public static void main(final String[] args){
        Pattern pattern;
        if(args.length < 1){
            pattern = Pattern.compile(".*");
        } else{
            pattern = Pattern.compile(args[0]);
        }
        final Collection<String> list = ResourceList.getResources(pattern);
        for(final String name : list){
            System.out.println(name);
        }
    }
}  

หากคุณกำลังใช้ Spring ลองดูที่PathMatchingResourcePatternResolver


3
Questioner รู้วิธีการนำไปใช้โดยใช้คลาส Java มาตรฐาน แต่เขาต้องการทราบว่าเขาสามารถใช้ห้องสมุด (Spring, Apache Commons) ที่มีอยู่ได้อย่างไร
Jeroen Rosenberg

@Jeroen โรเซนเบิร์กนอกจากนี้ยังมีวิธีการอื่นที่กำหนดซึ่งได้รับการคัดเลือกในที่สุด :)
Jigar Joshi

7
ฉันพบว่าวิธีนี้มีประโยชน์สำหรับกรณีที่คุณไม่ได้ใช้ Spring - มันจะไร้สาระที่จะเพิ่มการพึ่งพา Spring เนื่องจากคุณสมบัตินี้เท่านั้น ดังนั้นขอบคุณ! "สกปรก" แต่มีประโยชน์ :) PS หนึ่งอาจต้องการที่จะใช้File.pathSeparatorแทนการกำหนดค่าตายตัว:ในgetResourcesวิธีการ
Timur

5
ตัวอย่างโค้ดขึ้นอยู่กับระบบใช้สิ่งนี้แทน: final String[] classPathElements = classPath.split(System.pathSeparator);
dcompiled

1
Caveat: คุณสมบัติระบบ java.class.path อาจไม่มี classpath จริงที่คุณใช้งานอยู่ คุณสามารถดูที่ตัวโหลดคลาสและสอบถามค่าว่า URL ใดที่โหลดคลาสจาก ข้อแม้อื่น ๆ ของหลักสูตรคือถ้าคุณทำเช่นนี้ภายใต้ SecurityManager ตัวสร้าง ZipFile สามารถปฏิเสธการเข้าถึงไฟล์ของคุณดังนั้นคุณควรจะจับมัน
Trejkaz

24

การใช้ สะท้อนแสง

รับทุกสิ่งบน classpath:

Reflections reflections = new Reflections(null, new ResourcesScanner());
Set<String> resourceList = reflections.getResources(x -> true);

อีกตัวอย่างหนึ่ง - รับไฟล์ทั้งหมดที่มีนามสกุล. csvจากsome.package :

Reflections reflections = new Reflections("some.package", new ResourcesScanner());
Set<String> fileNames = reflections.getResources(Pattern.compile(".*\\.csv"));

มีวิธีใดที่จะประสบความสำเร็จเหมือนกันโดยไม่ต้องนำเข้าการพึ่งพาของ Google หรือไม่ ฉันต้องการหลีกเลี่ยงการพึ่งพานี้เพียงเพื่อทำงานนี้
Enrico Giurin

1
Reflections ไม่ใช่ห้องสมุดของ Google
ลุคฮัทชิสัน

อาจจะเป็นในอดีต คำตอบของฉันคือจากปี 2015 และฉันพบว่ามีการอ้างอิงถึงห้องสมุดโดยใช้วลี "Google Reflections" ก่อนปี 2015 ฉันเห็นว่าโค้ดย้ายไปมาเล็กน้อยและย้ายจาก code.google.com มาที่นี่เพื่อ github ที่นี่
NS du Toit

@ NSduToit ที่อาจเป็นสิ่งประดิษฐ์ของการโฮสต์รหัสใน Google Code (ไม่ใช่ความผิดพลาดที่ผิดปกติ) ไม่ใช่การเรียกร้องการประพันธ์โดย Google
matt b

17

หากคุณใช้ apache commonsIO คุณสามารถใช้สำหรับระบบไฟล์ (ตัวเลือกที่มีตัวกรองส่วนขยาย):

Collection<File> files = FileUtils.listFiles(new File("directory/"), null, false);

และสำหรับทรัพยากร / classpath:

List<String> files = IOUtils.readLines(MyClass.class.getClassLoader().getResourceAsStream("directory/"), Charsets.UTF_8);

หากคุณไม่ทราบว่า "directoy /" อยู่ในระบบไฟล์หรือในแหล่งข้อมูลคุณอาจเพิ่ม

if (new File("directory/").isDirectory())

หรือ

if (MyClass.class.getClassLoader().getResource("directory/") != null)

ก่อนการโทรและใช้ทั้งสองอย่างร่วมกัน ...


23
Files! = resources
djechlin

3
แน่นอนว่าทรัพยากรอาจไม่ใช่ไฟล์เสมอไป แต่คำถามก็คือทรัพยากรเกี่ยวกับที่มาจากไฟล์ / ไดเรกทอรี ดังนั้นให้ใช้โค้ดตัวอย่างถ้าคุณต้องการตรวจสอบตำแหน่งที่แตกต่างกันเช่นถ้าคุณมี config.xml ในทรัพยากรของคุณสำหรับการกำหนดค่าเริ่มต้นบางอย่างและมันควรจะเป็นไปได้ที่จะโหลด config.xml ภายนอกแทนถ้ามี ...
Rob

2
และทำไม resourses ไม่ใช่ไฟล์? Resourses เป็นไฟล์ที่เก็บลงในไฟล์ zip สิ่งเหล่านั้นถูกโหลดเข้าสู่หน่วยความจำและเข้าถึงได้ในวิธีเฉพาะ แต่ฉันไม่เห็นสาเหตุที่ไม่ใช่ไฟล์ exaple ของทรัพยากรใด ๆ ที่ไม่ใช่ 'ไฟล์ภายในที่เก็บถาวร'?
Mike

10
ไม่ทำงาน (NullPointerException) เมื่อทรัพยากรเป้าหมายเป็นไดเรกทอรีที่อยู่ในไฟล์ JAR (เช่น JAR มีแอปหลักและไดเรกทอรีเป้าหมาย)
Carlitos Way

12

ดังนั้นในแง่ของ PathMatchingResourcePatternResolver นี่คือสิ่งที่จำเป็นในรหัส:

@Autowired
ResourcePatternResolver resourceResolver;

public void getResources() {
  resourceResolver.getResources("classpath:config/*.xml");
}

เป็นเพียงส่วนเล็ก ๆ ถ้าคุณต้องการค้นหาแหล่งข้อมูลที่เป็นไปได้ทั้งหมดใน classpath ทั้งหมดที่คุณต้องการใช้classpath*:config/*.xml
revau.lt

5

Spring framework's PathMatchingResourcePatternResolverน่ากลัวจริงๆสำหรับสิ่งเหล่านี้:

private Resource[] getXMLResources() throws IOException
{
    ClassLoader classLoader = MethodHandles.lookup().getClass().getClassLoader();
    PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);

    return resolver.getResources("classpath:x/y/z/*.xml");
}

Maven พึ่งพา:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>LATEST</version>
</dependency>

5

ใช้การรวมกันของการตอบสนองของ Rob

final String resourceDir = "resourceDirectory/";
List<String> files = IOUtils.readLines(Thread.currentThread().getClass().getClassLoader().getResourceAsStream(resourceDir), Charsets.UTF_8);

for(String f : files){
  String data= IOUtils.toString(Thread.currentThread().getClass().getClassLoader().getResourceAsStream(resourceDir + f));
  ....process data
}

วิธีรับ ทรัพยากรทั้งหมด : เช่นเริ่มต้นด้วย. ' /' or หรือ./ไม่มีสิ่งใดที่ใช้งานได้จริง
javadba

3

กับฤดูใบไม้ผลิมันง่าย ไม่ว่าจะเป็นไฟล์หรือโฟลเดอร์หรือแม้แต่หลายไฟล์ก็มีโอกาสคุณสามารถทำได้ผ่านการฉีด

ตัวอย่างนี้แสดงการฉีดหลายไฟล์ที่อยู่ในx/y/zโฟลเดอร์

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

@Service
public class StackoverflowService {
    @Value("classpath:x/y/z/*")
    private Resource[] resources;

    public List<String> getResourceNames() {
        return Arrays.stream(resources)
                .map(Resource::getFilename)
                .collect(Collectors.toList());
    }
}

มันทำงานได้กับทรัพยากรในระบบแฟ้มเช่นเดียวกับใน JARs


2
ฉันต้องการรับชื่อโฟลเดอร์ภายใต้ / BOOT-INF / classes / static / Stories ฉันลองใช้รหัสของคุณด้วย @Value ("classpath: static / Stories / *") แล้วจะส่งคืนรายการว่างเมื่อเรียกใช้ JAR มันใช้งานได้กับทรัพยากรใน classpath แต่ไม่ใช่เมื่ออยู่ใน JAR
PS

@Value ("classpath: x / y / z / *") ทรัพยากรส่วนตัว [] ทำเพื่อฉัน มีชั่วโมงค้นหาที่! ขอบคุณ!
Rudolf Schmidt

3

สิ่งนี้จะใช้ได้ (ถ้าสปริงไม่ใช่ตัวเลือก):

public static List<String> getFilenamesForDirnameFromCP(String directoryName) throws URISyntaxException, UnsupportedEncodingException, IOException {
    List<String> filenames = new ArrayList<>();

    URL url = Thread.currentThread().getContextClassLoader().getResource(directoryName);
    if (url != null) {
        if (url.getProtocol().equals("file")) {
            File file = Paths.get(url.toURI()).toFile();
            if (file != null) {
                File[] files = file.listFiles();
                if (files != null) {
                    for (File filename : files) {
                        filenames.add(filename.toString());
                    }
                }
            }
        } else if (url.getProtocol().equals("jar")) {
            String dirname = directoryName + "/";
            String path = url.getPath();
            String jarPath = path.substring(5, path.indexOf("!"));
            try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8.name()))) {
                Enumeration<JarEntry> entries = jar.entries();
                while (entries.hasMoreElements()) {
                    JarEntry entry = entries.nextElement();
                    String name = entry.getName();
                    if (name.startsWith(dirname) && !dirname.equals(name)) {
                        URL resource = Thread.currentThread().getContextClassLoader().getResource(name);
                        filenames.add(resource.toString());
                    }
                }
            }
        }
    }
    return filenames;
}

สิ่งนี้ได้รับ downvote เมื่อเร็ว ๆ นี้ - หากคุณพบปัญหากรุณาแบ่งปันขอขอบคุณ!
fl0w

1
ขอบคุณ @ArnoUnkrig - โปรดแบ่งปันโซลูชันที่อัปเดตของคุณหากคุณต้องการ
fl0w

3

วิธีของฉันไม่มีสปริงใช้ระหว่างการทดสอบหน่วย:

URI uri = TestClass.class.getResource("/resources").toURI();
Path myPath = Paths.get(uri);
Stream<Path> walk = Files.walk(myPath, 1);
for (Iterator<Path> it = walk.iterator(); it.hasNext(); ) {
    Path filename = it.next();   
    System.out.println(filename);
}

ไม่ทำงานเมื่อคุณเรียกใช้ jar: java.nio.file.FileSystemNotFoundException ที่ Paths.get (uri)
error1009

1

กลไกที่แข็งแกร่งที่สุดสำหรับการแสดงรายการทรัพยากรทั้งหมดใน classpath ปัจจุบันใช้รูปแบบนี้กับ ClassGraphเนื่องจากเป็นเครื่องมือที่จัดการอาร์เรย์ข้อมูลจำเพาะ classpath ที่กว้างที่สุดเท่าที่จะเป็นไปได้รวมถึงระบบโมดูล JPMS ใหม่ (ฉันเป็นผู้เขียน ClassGraph)

List<String> resourceNames;
try (ScanResult scanResult = new ClassGraph().whitelistPaths("x/y/z").scan()) {
    resourceNames = scanResult.getAllResources().getNames();
}

0

ฉันคิดว่าคุณสามารถใช้ประโยชน์จาก [ ผู้ให้บริการระบบไฟล์ Zip ] [1] เพื่อให้บรรลุเป้าหมายนี้ เมื่อใช้งานFileSystems.newFileSystemดูเหมือนว่าคุณสามารถจัดการวัตถุใน ZIP นั้นเป็นไฟล์ "ปกติ"

ในเอกสารที่เชื่อมโยงด้านบน:

ระบุตัวเลือกการกำหนดค่าสำหรับระบบไฟล์ zip ในวัตถุ java.util.Map ที่ส่งไปยัง FileSystems.newFileSystemเมธอด ดูหัวข้อ [คุณสมบัติของระบบไฟล์ Zip] [2] สำหรับข้อมูลเกี่ยวกับคุณสมบัติการกำหนดค่าเฉพาะของผู้ให้บริการสำหรับระบบไฟล์ซิป

เมื่อคุณมีอินสแตนซ์ของระบบไฟล์ zip คุณสามารถเรียกใช้เมธอดของคลาส[ java.nio.file.FileSystem] [3] และ [ java.nio.file.Path] [4] เพื่อดำเนินการต่างๆเช่นการคัดลอกย้ายและเปลี่ยนชื่อไฟล์รวมถึงการแก้ไขคุณสมบัติไฟล์

เอกสารสำหรับjdk.zipfsโมดูลใน [Java 11 สถานะ] [5]:

ผู้ให้บริการระบบไฟล์ zip ถือว่าไฟล์ zip หรือ JAR เป็นระบบไฟล์และให้ความสามารถในการจัดการเนื้อหาของไฟล์ ผู้ให้บริการระบบไฟล์ zip สามารถสร้างได้โดย [ FileSystems.newFileSystem] [6] หากติดตั้งไว้

นี่คือตัวอย่างที่ฉันวางแผนไว้โดยใช้แหล่งข้อมูลตัวอย่างของคุณ โปรดทราบว่า a .zipคือ.jarแต่คุณสามารถปรับรหัสของคุณเพื่อใช้ทรัพยากร classpath แทน:

ติดตั้ง

cd /tmp
mkdir -p x/y/z
touch x/y/z/{a,b,c}.html
echo 'hello world' > x/y/z/d
zip -r example.zip x

ชวา

import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Collections;
import java.util.stream.Collectors;

public class MkobitZipRead {

  public static void main(String[] args) throws IOException {
    final URI uri = URI.create("jar:file:/tmp/example.zip");
    try (
        final FileSystem zipfs = FileSystems.newFileSystem(uri, Collections.emptyMap());
    ) {
      Files.walk(zipfs.getPath("/")).forEach(path -> System.out.println("Files in zip:" + path));
      System.out.println("-----");
      final String manifest = Files.readAllLines(
          zipfs.getPath("x", "y", "z").resolve("d")
      ).stream().collect(Collectors.joining(System.lineSeparator()));
      System.out.println(manifest);
    }
  }

}

เอาท์พุต

Files in zip:/
Files in zip:/x/
Files in zip:/x/y/
Files in zip:/x/y/z/
Files in zip:/x/y/z/c.html
Files in zip:/x/y/z/b.html
Files in zip:/x/y/z/a.html
Files in zip:/x/y/z/d
-----
hello world

0

ไม่มีคำตอบใดที่เหมาะกับฉันแม้ว่าฉันจะมีทรัพยากรของฉันใส่ไว้ในโฟลเดอร์ทรัพยากรและทำตามคำตอบข้างต้น สิ่งที่ทำให้เคล็ดลับคือ:

@Value("file:*/**/resources/**/schema/*.json")
private Resource[] resources;

-5

จากข้อมูลของ @rob ด้านบนฉันได้สร้างการใช้งานที่ฉันปล่อยสู่สาธารณสมบัติ:

private static List<String> getClasspathEntriesByPath(String path) throws IOException {
    InputStream is = Main.class.getClassLoader().getResourceAsStream(path);

    StringBuilder sb = new StringBuilder();
    while (is.available()>0) {
        byte[] buffer = new byte[1024];
        sb.append(new String(buffer, Charset.defaultCharset()));
    }

    return Arrays
            .asList(sb.toString().split("\n"))          // Convert StringBuilder to individual lines
            .stream()                                   // Stream the list
            .filter(line -> line.trim().length()>0)     // Filter out empty lines
            .collect(Collectors.toList());              // Collect remaining lines into a List again
}

แม้ว่าฉันจะไม่ได้คาดหวังว่าgetResourcesAsStreamจะทำงานแบบนั้นในไดเรกทอรี แต่ก็ใช้งานได้จริงและทำงานได้ดี

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.