Jaka wersja javac zbudowała mój słoik?


212

Skąd mam wiedzieć, która wersja kompilatora Java została użyta do zbudowania jar? Mam plik jar i mógł on zostać zbudowany w jednym z trzech JDK. Musimy dokładnie wiedzieć, który z nich, abyśmy mogli poświadczyć zgodność. Czy wersja kompilatora jest osadzona gdzieś w plikach klasy lub jar?


8
Możesz powiedzieć majorowi, patrząc na plik manifestu. Możesz określić wersję docelową, patrząc na same pliki klas, jednak JDK mogą tworzyć pliki klas dla wcześniejszych wersji Java za pomocą opcji -target, więc przeglądanie pierwszych bajtów może być niedokładne.
Peter Lawrey

15
W MANIFEST.MF można znaleźć coś takiegoCreated-By: 1.7.0_13 (Oracle Corporation)
hko19

1
Wygląda na to, że Maven 3 to robi Created-By: Apache MaveniBuild-Jdk: 1.8.0_25
Neal Xiong

Odpowiedzi:


89

Nie można tego stwierdzić na podstawie samego pliku JAR.

Pobierz edytor szesnastkowy i otwórz jeden z plików klas w pliku JAR i spójrz na przesunięcia bajtów od 4 do 7. Informacje o wersji są wbudowane.

http://en.wikipedia.org/wiki/Java_class_file

Uwaga: jak wspomniano w komentarzu poniżej,

bajty te informują o wersji, dla której klasa została skompilowana, a nie w jakiej wersji.


41
Mówiąc pedantycznie, bajty te informują o wersji, dla której klasa została skompilowana, a nie w jakiej wersji. Java pozwala na kompilację kodu, aby był zgodny z wcześniejszymi wersjami Java. Dotyczy to jednak tylko kodu bajtowego i formatu. Na przykład z przyjemnością skompiluje kod odwołujący się do bibliotek JDK 6 do formatu JDK 5. JDK 5 załaduje klasę, ale nie może go uruchomić, ponieważ biblioteka JDK 5 nie ma kodu, do którego odwołuje się JDK 6.
Will Hartung,

6
Możemy go znaleźć w pliku manifestu jako Created-By: 1.7.0_21-b11 (Oracle Corporation)
Krishna

8
Druga odpowiedź mówi, jak łatwo sprawdzić za pomocą wiersza poleceń
mgarciaisaia

313

A jarto tylko pojemnik. Jest to archiwum plików la tar. Chociaż jarmoże zawierać interesujące informacje zawarte w hierarchii META-INF , nie ma obowiązku określania rocznika klas w swojej treści. W tym celu należy sprawdzić zawarte w classnim pliki.

Jak wspomniał Peter Lawrey w komentarzu do pierwotnego pytania, niekoniecznie musisz wiedzieć, która wersja JDK zbudowała dany classplik, ale możesz znaleźć wersję klasy bajtowej classpliku zawartego w pliku jar.

Tak, to trochę do bani, ale pierwszym krokiem jest wyodrębnienie jednej lub więcej klas z jar. Na przykład:

$ jar xf log4j-1.2.15.jar

W systemie Linux, Mac OS X lub Windows z zainstalowanym Cygwin polecenie file (1) zna wersję klasy.

$ file ./org/apache/log4j/Appender.class
./org/apache/log4j/Appender.class: compiled Java class data, version 45.3

Lub alternatywnie, używając javapz JDK jako @ jikes.thunderbolt trafnie wskazuje:

$ javap -v ./org/apache/log4j/Appender.class | grep major
 major version: 45

A jeśli zostaniesz przeniesiony do Windowsśrodowiska bez jednego filelubgrep

> javap -v ./org/apache/log4j/Appender.class | findstr major
 major version: 45

FWIW, zgodzę się, że javappowie o wiele więcej o danym classpliku niż pierwotne pytanie.

W każdym razie inna wersja klasy, na przykład:

$ file ~/bin/classes/P.class
/home/dave/bin/classes/P.class: compiled Java class data, version 50.0

Numer główny wersji klasy odpowiada następującym wersjom JDK Java:

  • 45,3 = Java 1.1
  • 46 = Java 1.2
  • 47 = Java 1.3
  • 48 = Java 1.4
  • 49 = Java 5
  • 50 = Java 6
  • 51 = Java 7
  • 52 = Java 8
  • 53 = Java 9

7
Moja wersja filenie wykazały, ale udało mi się ręcznie sprawdzić klasę z tym poleceniem: hexdump ~/bin/classes/P.class | head. Wystarczy spojrzeć na ósmy bajt i przekonwertować na dziesiętny.
Jarett Millard

2
Wydaje się, że „plik” FYI zależy od wersji Javy, czy będzie wyświetlać również wersję JDK. Na CentOS / Oracle Linux i klasie skompilowanej w Javie 6 otrzymuję „skompilowane dane klasy Java, wersja 50.0 (Java 1.6)”, ale kiedy uruchamiam ją w klasie skompilowanej w Javie 7, otrzymuję tylko wersję ogólną ”skompilowane dane klasy Java, wersja 51.0 "
Dan Haynes

1
JAR=something.jar ; unzip -p $JAR `unzip -l $JAR | grep '\.class$' | head -1` | file -
Randall Whitman

3
@JarettMillard mój cygwin file file.class(5.22-1) początkowo nie pokazywał docelowego celu klasy Java: „file.class: [architecture = 6909806] [architecture = 6845039]”. Jednak file -k file.classzrobił to: „file.class: [architektura = 6909806] [architektura = 6845039] skompilował dane klasy Java, wersja 50.0 (Java 1.6)”. Wygląda na to, że filedostał tylko pierwszy mecz w swojej bazie danych magicfiles bez -k.
dim

1
Używanie „javap -p <klasa> | grep major” nie jest niezawodne. Jeśli plik pom.xml ma źródło / cel 1.7, javap zawsze da ci „wersję główną: 51” niezależnie od tego, czy do kompilacji używasz JDK 1.7, JDK 1.8 czy JDK 1.9.
user2569618,

54

Oto sposób, w jaki Java może znaleźć te informacje.

Windows: javap -v <class> | findstr major
Unix:javap -v <class> | grep major

Na przykład:
> javap -v Application | findstr major   major version: 51


3
spośród wszystkich udzielonych odpowiedzi twoja odpowiedź jest najbardziej zwięzła i nie wymaga pisania 10 linii kodu, aby poznać informacje o wersji. +1 za to
Thirumalai Parthasarathi

2
Do czego służy <class>parametr?
Przyciemnia

1
@Dims Plik „.class”, dla którego próbujesz znaleźć wersję. W swoim przykładzie ma Application.classplik i okazało się, że został skompilowany dla Java 7 ( major version: 51).
inanutshellus

1
Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

15

Nie ma potrzeby rozpakowywania pliku JAR (jeśli jedna z nazw klas jest znana lub jest sprawdzana, np. Za pomocą 7zip), więc w systemie Windows wystarczające byłyby:

javap -cp log4j-core-2.5.jar -verbose org.apache.logging.log4j.core.Logger | findstr major

1
Działa to również na Linuksie (chyba że użyłbyś grep zamiast findstr oczywiście)
Michael Rusch

1
Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

15

Kompilator Java ( javac) nie buduje słoików, tłumaczy pliki Java na pliki klas. Narzędzie Jar ( jar) tworzy rzeczywiste słoiki. Jeśli nie określono żadnego niestandardowego manifestu, domyślny manifest określa, która wersja JDK została użyta do utworzenia jar.


3
Chociaż jest to prawda, nie daje to odpowiedzi na pytanie.
zb226,

2
@ zb226 AFAIK wersja kompilatora użyte do kompilacji klas nie można uzyskać z plików klasowych, tylko w wersji, że zajęcia zostały skompilowane dla . Podane przeze mnie informacje zawierają jedyne (standardowe) informacje, które są dostępne na temat tego, która wersja zbudowała artefakt. Jeśli nie o to pytamy, pytanie należy przeredagować.
jackrabbit

OK, patrzyłem na to z pragmatycznego punktu widzenia, ale teraz widzę twój punkt, głosowanie usunięte :)
zb226

9

Ponieważ musiałem przeanalizować grube słoiki, byłem zainteresowany wersją każdej klasy w pliku jar. Dlatego podjąłem podejście Joe Liversedge https://stackoverflow.com/a/27877215/1497139 i połączyłem je z https://stackoverflow.com/a/3313839/1497139 tabelą wersji klas klasy Davida J. Liszewskiego, aby utworzyć skrypt bash jarv, aby wyświetlić wersje wszystkich plików klas w pliku jar.

stosowanie

usage: ./jarv jarfile
 -h|--help: show this usage

Przykład

jarv $Home/.m2/repository/log4j/log4j/1.2.17/log4j-1.2.17.jar

java 1.4 org.apache.log4j.Appender
java 1.4 org.apache.log4j.AppenderSkeleton
java 1.4 org.apache.log4j.AsyncAppender$DiscardSummary
java 1.4 org.apache.log4j.AsyncAppender$Dispatcher
...

Skrypt Bash jarv

#!/bin/bash
# WF 2018-07-12
# find out the class versions with in jar file
# see https://stackoverflow.com/questions/3313532/what-version-of-javac-built-my-jar

# uncomment do debug
# set -x

#ansi colors
#http://www.csc.uvic.ca/~sae/seng265/fall04/tips/s265s047-tips/bash-using-colors.html
blue='\033[0;34m'  
red='\033[0;31m'  
green='\033[0;32m' # '\e[1;32m' is too bright for white bg.
endColor='\033[0m'

#
# a colored message 
#   params:
#     1: l_color - the color of the message
#     2: l_msg - the message to display
#
color_msg() {
  local l_color="$1"
  local l_msg="$2"
  echo -e "${l_color}$l_msg${endColor}"
}

#
# error
#
#   show an error message and exit
#
#   params:
#     1: l_msg - the message to display
error() {
  local l_msg="$1"
  # use ansi red for error
  color_msg $red "Error: $l_msg" 1>&2
  exit 1
}

#
# show the usage
#
usage() {
  echo "usage: $0 jarfile"
  # -h|--help|usage|show this usage
  echo " -h|--help: show this usage"
  exit 1 
}

#
# showclassversions
#
showclassversions() {
  local l_jar="$1"
  jar -tf "$l_jar" | grep '.class' | while read classname
  do
    class=$(echo $classname | sed -e 's/\.class$//')
    class_version=$(javap -classpath "$l_jar" -verbose $class | grep 'major version' | cut -f2 -d ":" | cut -c2-)
    class_pretty=$(echo $class | sed -e 's#/#.#g')
    case $class_version in
      45.3) java_version="java 1.1";;
      46) java_version="java 1.2";;
      47) java_version="java 1.3";;
      48) java_version="java 1.4";;
      49) java_version="java5";;
      50) java_version="java6";;
      51) java_version="java7";;
      52) java_version="java8";;
      53) java_version="java9";;
      54) java_version="java10";;
      *) java_version="x${class_version}x";;
    esac
    echo $java_version $class_pretty
  done
}

# check the number of parameters
if [ $# -lt 1 ]
then
  usage
fi

# start of script
# check arguments
while test $# -gt 0
do
  case $1 in
    # -h|--help|usage|show this usage
    -h|--help) 
      usage
      exit 1
      ;;
    *)
     showclassversions "$1"
  esac
  shift
done 

1
Świetny scenariusz! Byłem w stanie sprawdzić wersje słoików w Javie.
ARK

Dziękuję bardzo za udostępnienie skryptu! Tylko wskazówka z mojej strony. head -1może być używany w połączeniu ze skryptem: zwykle wystarczy zobaczyć wersję pierwszej klasy w pliku jar.
Sergey Brunov

7

wersję kompilatora Java można znaleźć w plikach .class za pomocą edytora heksadecymalnego.

Krok 1: Wyodrębnij pliki .class z pliku jar za pomocą ekstraktora zip

krok 2: otwórz plik .class w edytorze szesnastkowym. (Użyłem wtyczki edytora szesnastkowego notatnika ++. Ta wtyczka odczytuje plik jako plik binarny i pokazuje go w postaci szesnastkowej). Możesz zobaczyć poniżej. wprowadź opis zdjęcia tutaj

Indeks 6 i 7 podaje główny numer wersji używanego formatu pliku klasy. https://en.wikipedia.org/wiki/Java_class_file

Java SE 11 = 55 (0x37 hex)

Java SE 10 = 54 (0x36 hex)

Java SE 9 = 53 (0x35 hex)

Java SE 8 = 52 (0x34 hex),

Java SE 7 = 51 (0x33 hex),

Java SE 6.0 = 50 (0x32 hex),

Java SE 5.0 = 49 (0x31 hex),

JDK 1.4 = 48 (0x30 hex),

JDK 1.3 = 47 (0x2F hex),

JDK 1.2 = 46 (0x2E hex),

JDK 1.1 = 45 (0x2D hex).



4

Kod wysłane przez Owena można powiedzieć informacje wymienione przez szereg innych odpowiedzi tutaj:

public void simpleExample ()
{
    FileInputStream fis = new FileInputStream ("mytest.class");
    parseJavaClassFile ( fis );
}
protected void parseJavaClassFile ( InputStream classByteStream ) throws Exception
{
    DataInputStream dataInputStream = new DataInputStream ( classByteStream );
    magicNumber = dataInputStream.readInt();
    if ( magicNumber == 0xCAFEBABE )
    {
        int minorVer = dataInputStream.readUnsignedShort();
        int majorVer = dataInputStream.readUnsignedShort();
        // do something here with major & minor numbers
    }
}

Zobacz także i stronę. Skończyło się na szybkim modyfikowaniu kodu produktów umysłowych, aby sprawdzić, do czego została skompilowana każda z moich zależności.


Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

3

Programiści i administratorzy używający Bash mogą uznać te funkcje wygody za pomocne:

jar_jdk_version() {
  [[ -n "$1" && -x "`command -v javap`" ]] && javap -classpath "$1" -verbose $(jar -tf "$1" | grep '.class' | head -n1 | sed -e 's/\.class$//') | grep 'major version' | sed -e 's/[^0-9]\{1,\}//'
}

print_jar_jdk_version() {
  local version
  version=$(jar_jdk_version "$1")
  case $version in 49) version=1.5;; 50) version=1.6;; 51) version=1.7;; 52) version=1.8;; esac
  [[ -n "$version" ]] && echo "`basename "$1"` contains classes compiled with JDK version $version."
}

Możesz je wkleić do jednorazowego użytku lub dodać do ~/.bash_aliaseslub ~/.bashrc. Wyniki wyglądają mniej więcej tak:

$ jar_jdk_version poi-ooxml-3.5-FINAL.jar
49

i

$ print_jar_jdk_version poi-ooxml-3.5-FINAL.jar
poi-ooxml-3.5-FINAL.jar contains classes compiled with JDK version 1.5.

EDYCJA Jak wskazuje jackrabbit , nie możesz w 100% polegać na manifeście, aby powiedzieć ci coś pożytecznego. Jeśli tak, to możesz wyciągnąć go w swojej ulubionej powłoce UNIX za pomocą unzip:

$ unzip -pa poi-ooxml-3.5-FINAL.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.7.1
Created-By: 11.3-b02 (Sun Microsystems Inc.)
Built-By: yegor
Specification-Title: Apache POI
Specification-Version: 3.5-FINAL-20090928
Specification-Vendor: Apache
Implementation-Title: Apache POI
Implementation-Version: 3.5-FINAL-20090928
Implementation-Vendor: Apache

Ten .jar nie ma nic użytecznego w manifeście na temat zawartych klas.


Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

3

Jeden liner (Linux)

unzip -p mylib.jar META-INF/MANIFEST.MF

Spowoduje to wydrukowanie zawartości MANIFEST.MFpliku na standardowe wyjście (mam nadzieję, że plik znajduje się w pliku jar :)

W zależności od tego, co zbudowało twój pakiet, znajdziesz wersję JDK w Created-Bylub Build-Jdkkey.


3
Zauważ, że te pola nie są zobowiązane do obecności MANIFEST.MF, a także nie ma obowiązku poprawności wartości (Tak, kiedyś spotkałem się .jarz fałszywą wartością).
zb226,

Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

2

Każdy plik klasy ma numer wersji osadzony na poziomie kodu bajtowego, którego JVM używa, aby sprawdzić, czy podoba mu się ten konkretny fragment kodu bajtowego, czy nie. To 48 dla Java 1.4, 49 dla Java 1.5 i 50 dla Java 6.

Istnieje wiele kompilatorów, które mogą generować kod bajtowy na każdym poziomie, javac używa opcji „-target”, aby wskazać, który poziom kodu bajtowego ma zostać wygenerowany, a javac Java 6 może generować kod bajtowy dla przynajmniej 1.4, 1.5 i 6. Nie wierzę, że kompilator wstawia wszystko, co może zidentyfikować sam kompilator, i myślę, że o to prosisz. Coraz częściej stosuje się także kompilator Eclipse, ponieważ jest to pojedynczy słoik, który może działać tylko z JRE.

W pliku jar znajduje się zwykle wiele klas, a każda z nich jest niezależna, dlatego musisz sprawdzić wszystkie klasy w słoiku, aby mieć pewność co do właściwości zawartości.


1

W odpowiedzi na odpowiedź Davida J. Liszewskiego uruchomiłem następujące polecenia, aby wyodrębnić manifest pliku jar na Ubuntu:

# Determine the manifest file name:
$ jar tf LuceneSearch.jar | grep -i manifest
META-INF/MANIFEST.MF

# Extract the file:
$ sudo jar xf LuceneSearch.jar META-INF/MANIFEST.MF

# Print the file's contents:
$ more META-INF/MANIFEST.MF
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.2
Created-By: 1.7.0_25-b30 (Oracle Corporation)
Main-Class: org.wikimedia.lsearch.config.StartupManager

2
Lub, jako jeden unzip -p LiceneSearch.jar META-INF/MANIFEST.MF
wiersz

Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

1

Wiele razy możesz patrzeć na całe pliki jar lub pliki wojenne zawierające wiele plików jar oprócz siebie.

Ponieważ nie chciałem ręcznie sprawdzać każdej klasy, napisałem program Java, aby to zrobić:

https://github.com/Nthalk/WhatJDK

./whatjdk some.war
some.war:WEB-INF/lib/xml-apis-1.4.01.jar contains classes compatible with Java1.1
some.war contains classes compatible with Java1.6

Chociaż nie mówi to o tym, z czym klasa została skompilowana, określa, jakie JDK będą mogły ZAŁADOWAĆ klasy, co prawdopodobnie jest tym, od czego chciałeś zacząć.


1

Aby rozwinąć odpowiedzi Jonathona Fausta i McDowell : Jeśli korzystasz z systemu opartego na * nix, możesz użyć od(jednego z pierwszych programów uniksowych 1, który powinien być dostępny praktycznie wszędzie), aby wysłać zapytanie do .classpliku na poziomie binarnym:

od -An -j7 -N1 -t dC SomeClassFile.class

Spowoduje to wyświetlenie znanych wartości całkowitych, np. 50For Java 5, 51for Java 6i tak dalej.

1 Cytat z https://en.wikipedia.org/wiki/Od_(Unix)


Te polecenia dostarczają docelową wersję JVM, a nie javacwersję, która skompilowała pliki .class, o co proszono.
Markiz Lorne

1

Napisałem również własny skrypt bash, aby zrzucić wersję Java wymaganą przez wszystkie słoiki przekazane w wierszu poleceń ... Mój jest trochę szorstki, ale dla mnie działa ;-)

przykładowe użycie

$ jar_dump_version_of_jvm_required.sh *.jar
JVM VERSION REQUIRED: 46.0, /private/tmp/jars/WEB-INF/lib/json-simple-1.1.jar
JVM VERSION REQUIRED: 49.0, /private/tmp/jars/WEB-INF/lib/json-smart-1.1.1.jar
JVM VERSION REQUIRED: 50.0, /private/tmp/jars/WEB-INF/lib/jsontoken-1.0.jar
JVM VERSION REQUIRED: 50.0, /private/tmp/jars/WEB-INF/lib/jsr166y-1.7.0.jar

jar_dump_version_of_jvm_required.sh

#!/bin/bash

DIR=$(PWD)
function show_help()
{
  ME=$(basename $0)
  IT=$(cat <<EOF

  Dumps the version of the JVM required to run the classes in a jar file

  usage: $ME JAR_FILE

  e.g. 

  $ME myFile.jar    ->  VERSION: 50.0     myFile.jar

  Java versions are:
  54 = Java 10
  53 = Java 9
  52 = Java 8
  51 = Java 7
  50 = Java 6
  49 = Java 5
  48 = Java 1.4
  47 = Java 1.3
  46 = Java 1.2
  45.3 = Java 1.1

EOF
  )
  echo "$IT"
  exit
}

if [ "$1" == "help" ]
then
  show_help
fi
if [ -z "$1" ]
then
  show_help
fi

function unzipJarToTmp()
{
  JAR=$1
  CLASS_FILE=$(jar -tf "$JAR" | grep \.class$ | grep -v '\$' | head -n1 | awk '{print $NF}')
  OUT_FILE="$CLASS_FILE"
  #echo "J=$JAR C=$CLASS_FILE O=$OUT_FILE"
  jar xf "$JAR" "$CLASS_FILE"

  MAJOR=$(javap -v "$OUT_FILE" 2>&1 | grep major | awk -F' ' '{print $3'})
  MINOR=$(javap -v "$OUT_FILE" 2>&1 | grep minor | awk -F' ' '{print $3'})
  if [ -z "$MAJOR" ]
  then
    echo "JVM VERSION REQUIRED: NA as no classes in $JAR"
  else
    echo "JVM VERSION REQUIRED: $MAJOR.$MINOR, $JAR"
  fi
}

# loop over cmd line args
for JAR in "$@"
do
  cd "$DIR"
  JAR_UID=$(basename "$JAR" | sed s/.jar//g)
  TMPDIR=/tmp/jar_dump/$JAR_UID/
  mkdir -p "$TMPDIR"
  JAR_ABS_PATH=$(realpath $JAR)

  cd "$TMPDIR"

  #echo "$JAR_ABS_PATH"
  unzipJarToTmp "$JAR_ABS_PATH"
  #sleep 2
done

1

Możesz to łatwo zrobić w wierszu poleceń, wykonując następujący proces:

Jeśli znasz dowolną nazwę klasy w słoiku, możesz użyć następującego polecenia:

javap -cp jarname.jar -verbose packagename.classname | findstr major

przykład:

    C:\pathwherejarlocated> javap -cp jackson-databind-2.8.6.jar -verbose com.fasterxml.jackson.databind.JsonMappingException | findstr major

Wynik :

    major version: 51

Krótki przegląd:

JDK 1.0  major version 45 
DK 1.1  major version 45 
JDK 1.2  major version 46 
JDK 1.3  major version 47 
JDK 1.4  major version 48 
JDK 1.5  major version 49 
JDK 1.6  major version 50 
JDK 1.7  major version 51 
JDK 1.8  major version 52 
JDK 1.9  major version 53 

PS: jeśli nie znasz żadnej z nazw klas, możesz to łatwo zrobić za pomocą dowolnego dekompilatora jar lub po prostu używając następującego polecenia, aby wyodrębnić plik jar:

jar xf myFile.jar

0

Sprawdzasz w pliku manifestu przykład słoika:

Wersja manifestu: 1.0 Utworzono: 1.6.0 (IBM Corporation)


-1

W systemie Windows wykonaj następujące czynności:

  1. Rozpakuj lub rozpakuj plik JAR za pomocą komendy JAR WinZip / Java.
  2. Przeciągnij i upuść jeden z plików klas do projektu Java Eclipse.
  3. Otwórz plik klasy.

Teraz Eclipse wyświetli dokładną wersję główną i dodatkową.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.