>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){"use strict";e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)});
\ No newline at end of file
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/AttestationService.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/AttestationService.java
new file mode 100644
index 0000000000..c865a01e88
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/AttestationService.java
@@ -0,0 +1,48 @@
+package ch.unifr.digits.webprotege.attestation.server;
+
+import ch.unifr.digits.webprotege.attestation.shared.VerifyResult;
+import okhttp3.OkHttpClient;
+import org.web3j.crypto.Credentials;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.Web3jService;
+import org.web3j.protocol.core.methods.response.TransactionReceipt;
+import org.web3j.protocol.http.HttpService;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static ch.unifr.digits.webprotege.attestation.server.SettingsManager.*;
+
+public abstract class AttestationService {
+
+ protected static final Credentials CREDENTIALS = Credentials.create(SERVER_SECRET);
+ protected static final AtomicReference WEB3_REF = new AtomicReference<>();
+
+ public AttestationService() {
+ init();
+ }
+
+ public abstract String contractAddress();
+ public abstract TransactionReceipt attest(String iri, String versionIri, String name, String hash, T params) throws Exception;
+ public abstract VerifyResult verify(String iri, String versionIri, String hash, T params) throws Exception;
+
+ private static void init() {
+ if (WEB3_REF.compareAndSet(null, null)) {
+ Web3jService service = buildService(PROVIDER_HOST + ":" + PROVIDER_PORT);
+ Web3j web3 = Web3j.build(service);
+ boolean result = WEB3_REF.compareAndSet(null, web3);
+ if (!result) web3.shutdown();
+ }
+ }
+
+ private static Web3jService buildService(String url) {
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
+ long tos = 300L;
+ builder.connectTimeout(tos, TimeUnit.SECONDS);
+ builder.readTimeout(tos, TimeUnit.SECONDS);
+ builder.writeTimeout(tos, TimeUnit.SECONDS);
+ return new HttpService(url, builder.build());
+ }
+
+ public static class NopParam {}
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/ChangeTrackingAttestationService.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/ChangeTrackingAttestationService.java
new file mode 100644
index 0000000000..d18b37ce4a
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/ChangeTrackingAttestationService.java
@@ -0,0 +1,87 @@
+package ch.unifr.digits.webprotege.attestation.server;
+
+import ch.unifr.digits.webprotege.attestation.server.contracts.ChangeTracking;
+import ch.unifr.digits.webprotege.attestation.server.contracts.OntologyAttestation;
+import ch.unifr.digits.webprotege.attestation.shared.VerifyResult;
+import org.semanticweb.owlapi.model.IRI;
+import org.semanticweb.owlapi.model.OWLClass;
+import org.semanticweb.owlapi.model.OWLOntology;
+import org.web3j.protocol.core.RemoteFunctionCall;
+import org.web3j.protocol.core.methods.response.TransactionReceipt;
+import org.web3j.tuples.generated.Tuple4;
+import org.web3j.tx.gas.DefaultGasProvider;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static ch.unifr.digits.webprotege.attestation.server.SettingsManager.ADDRESS_CHANGETRACKING;
+
+public class ChangeTrackingAttestationService extends OntologyAttestationService {
+
+ @Override
+ public String contractAddress() {
+ return ADDRESS_CHANGETRACKING;
+ }
+
+ public VerifyResult verifyEntity(String iri, String versionIri, String entityHash) throws Exception {
+ ChangeTracking contract = ChangeTracking.load(contractAddress(), WEB3_REF.get(), CREDENTIALS,
+ DefaultGasProvider.GAS_PRICE, DefaultGasProvider.GAS_LIMIT);
+ RemoteFunctionCall> remoteFunctionCall =
+ contract.verifyEntity(iri, versionIri, new BigInteger(entityHash));
+ Tuple4 result = remoteFunctionCall.send();
+ BigInteger bigInteger = result.component4();
+ int time = (bigInteger == null) ? -1 : bigInteger.intValue();
+ return new VerifyResult(result.component1(), result.component2(), result.component3(), time);
+ }
+
+ @Override
+ public VerifyResult verify(String iri, String versionIri, String hash, EntitySet params) throws Exception {
+ ChangeTracking contract = ChangeTracking.load(contractAddress(), WEB3_REF.get(), CREDENTIALS,
+ DefaultGasProvider.GAS_PRICE, DefaultGasProvider.GAS_LIMIT);
+ RemoteFunctionCall> remoteFunctionCall =
+ contract.verify(iri, versionIri, hash);
+ Tuple4 result = remoteFunctionCall.send();
+ BigInteger bigInteger = result.component4();
+ int time = (bigInteger == null) ? -1 : bigInteger.intValue();
+ return new VerifyResult(result.component1(), result.component2(), result.component3(), time);
+ }
+
+ @Override
+ public TransactionReceipt attest(OWLOntology ontology, String name) throws Exception {
+ String hash = ontologyHash(ontology);
+ String ontologyIri = ontology.getOntologyID().getOntologyIRI().get().toString();
+ String versionIri = ontology.getOntologyID().getVersionIRI().transform(IRI::toString).or("");
+ List classHashes = toBigInt(classHashes(ontology));
+ return attest(ontologyIri, versionIri, name, hash, new EntitySet(classHashes));
+ }
+
+ @Override
+ public TransactionReceipt attest(String iri, String versionIri, String name, String hash, EntitySet params) throws Exception {
+ ChangeTracking contract = ChangeTracking.load(contractAddress(), WEB3_REF.get(), CREDENTIALS,
+ DefaultGasProvider.GAS_PRICE, new BigInteger("1000000000"));
+ RemoteFunctionCall call = contract.attest(iri, versionIri, name, hash,
+ params.hashes);
+ TransactionReceipt transactionReceipt = call.send();
+ return transactionReceipt;
+ }
+
+ public List toBigInt(List ints) {
+ List classHashes = ints.stream().map(BigInteger::valueOf).collect(Collectors.toList());
+ return classHashes;
+ }
+
+ public List classHashes(OWLOntology ontology) {
+ Set classesInSignature = ontology.getClassesInSignature();
+ List classHashes = classesInSignature.stream().map(this::entityHash).collect(Collectors.toList());
+ return classHashes;
+ }
+
+ public static class EntitySet {
+ public EntitySet(List hashes) {
+ this.hashes = hashes;
+ }
+ public final List hashes;
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/FileAttestationService.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/FileAttestationService.java
new file mode 100644
index 0000000000..1b68453c10
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/FileAttestationService.java
@@ -0,0 +1,64 @@
+package ch.unifr.digits.webprotege.attestation.server;
+
+import ch.unifr.digits.webprotege.attestation.server.contracts.OntologyAttestation;
+import ch.unifr.digits.webprotege.attestation.shared.VerifyResult;
+import org.web3j.protocol.core.RemoteFunctionCall;
+import org.web3j.protocol.core.methods.response.TransactionReceipt;
+import org.web3j.tuples.generated.Tuple4;
+import org.web3j.tx.gas.DefaultGasProvider;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import static ch.unifr.digits.webprotege.attestation.server.SettingsManager.ADDRESS_ATTESTATION;
+
+public class FileAttestationService extends AttestationService {
+
+ @Override
+ public String contractAddress() {
+ return ADDRESS_ATTESTATION;
+ }
+
+ @Override
+ public TransactionReceipt attest(String iri, String versionIri, String name, String hash, T params) throws Exception {
+ OntologyAttestation contract = OntologyAttestation.load(contractAddress(), WEB3_REF.get(), CREDENTIALS,
+ DefaultGasProvider.GAS_PRICE, DefaultGasProvider.GAS_LIMIT);
+ RemoteFunctionCall call = contract.attest(iri, versionIri, name, hash);
+ TransactionReceipt receipt = call.send();
+ return receipt;
+ }
+
+ @Override
+ public VerifyResult verify(String iri, String versionIri, String hash, T params) throws Exception {
+ OntologyAttestation contract = OntologyAttestation.load(contractAddress(), WEB3_REF.get(), CREDENTIALS,
+ DefaultGasProvider.GAS_PRICE, DefaultGasProvider.GAS_LIMIT);
+ RemoteFunctionCall> remoteFunctionCall =
+ contract.verify(iri, versionIri, hash);
+ Tuple4 result = remoteFunctionCall.send();
+ BigInteger bigInteger = result.component4();
+ int time = (bigInteger == null) ? -1 : bigInteger.intValue();
+ return new VerifyResult(result.component1(), result.component2(), result.component3(), time);
+ }
+
+ public String hashFile(byte[] data) {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(data);
+ return bytesToHex(hashBytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String bytesToHex(byte[] hash) {
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/HashStringUtils.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/HashStringUtils.java
new file mode 100644
index 0000000000..537d0009fb
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/HashStringUtils.java
@@ -0,0 +1,32 @@
+package ch.unifr.digits.webprotege.attestation.server;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class HashStringUtils {
+
+ public static String boundedHex(int data) {
+ return bytesToHex(hash(BigInteger.valueOf(data).toByteArray()));
+ }
+
+ public static byte[] hash(byte[] data) {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ return digest.digest(data);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String bytesToHex(byte[] hash) {
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/OntologyAttestationService.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/OntologyAttestationService.java
new file mode 100644
index 0000000000..f2fdae5605
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/OntologyAttestationService.java
@@ -0,0 +1,48 @@
+package ch.unifr.digits.webprotege.attestation.server;
+
+import ch.unifr.digits.webprotege.attestation.server.compression.tree.CompressionTree;
+import ch.unifr.digits.webprotege.attestation.server.compression.tree.OWLToRDFTranslator;
+import ch.unifr.digits.webprotege.attestation.server.compression.tree.RDFTriple;
+import org.semanticweb.owlapi.model.*;
+import org.semanticweb.owlapi.model.parameters.Imports;
+import org.web3j.protocol.core.methods.response.TransactionReceipt;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class OntologyAttestationService extends FileAttestationService {
+
+ public TransactionReceipt attest(OWLOntology ontology, String name) throws Exception {
+ String hash = ontologyHash(ontology);
+ String ontologyIri = ontology.getOntologyID().getOntologyIRI().get().toString();
+ String versionIri = ontology.getOntologyID().getVersionIRI().transform(IRI::toString).or("");
+ return attest(ontologyIri, versionIri, name, hash, null);
+ }
+
+ public String ontologyHash(OWLOntology ontology) {
+ try {
+ OWLToRDFTranslator translator = new OWLToRDFTranslator(ontology);
+ RDFTriple[] triples = translator.getTriples();
+ CompressionTree ct = new CompressionTree(triples);
+ return ct.getRoot();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public String simpleOntologyHash(OWLOntology ontology) {
+ Set signature = ontology.getSignature(Imports.INCLUDED);
+ Set axioms = ontology.getAxioms(Imports.INCLUDED);
+ Set annotations = ontology.getAnnotations();
+ Set all = new HashSet<>();
+ all.addAll(signature);
+ all.addAll(annotations);
+ all.addAll(axioms);
+ return HashStringUtils.boundedHex(all.hashCode());
+ }
+
+ public int entityHash(OWLEntity entity) {
+ return entity.hashCode();
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/SettingsManager.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/SettingsManager.java
new file mode 100644
index 0000000000..30884f9feb
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/SettingsManager.java
@@ -0,0 +1,39 @@
+package ch.unifr.digits.webprotege.attestation.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
+
+public class SettingsManager {
+
+ public static final String PROVIDER_HOST;
+ public static final String PROVIDER_PORT;
+ public static final String ADDRESS_ATTESTATION;
+ public static final String ADDRESS_CHANGETRACKING;
+ public static final String SERVER_SECRET;
+
+ private static final String PROVIDER_HOST_VARNAME = "PROVIDER_HOST";
+ private static final String PROVIDER_PORT_VARNAME = "PROVIDER_PORT";
+ private static final String ADDRESS_ATTESTATION_VARNAME = "ADDRESS_ATTESTATION";
+ private static final String ADDRESS_CHANGETRACKING_VARNAME = "ADDRESS_CHANGETRACKING";
+ private static final String SERVER_SECRET_VARNAME = "SERVER_SECRET";
+
+ static {
+ final Map env = System.getenv();
+ final Properties properties = new Properties();
+ try (InputStream stream = SettingsManager.class.getClassLoader()
+ .getResourceAsStream("configuration/config.properties")) {
+ properties.load(stream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ PROVIDER_HOST = env.getOrDefault(PROVIDER_HOST_VARNAME, properties.getProperty(PROVIDER_HOST_VARNAME));
+ PROVIDER_PORT = env.getOrDefault(PROVIDER_PORT_VARNAME, properties.getProperty(PROVIDER_PORT_VARNAME));
+ ADDRESS_ATTESTATION = env.getOrDefault(ADDRESS_ATTESTATION_VARNAME, properties.getProperty(ADDRESS_ATTESTATION_VARNAME));
+ ADDRESS_CHANGETRACKING = env.getOrDefault(ADDRESS_CHANGETRACKING_VARNAME, properties.getProperty(ADDRESS_CHANGETRACKING_VARNAME));
+ SERVER_SECRET = env.getOrDefault(SERVER_SECRET_VARNAME, properties.getProperty(SERVER_SECRET_VARNAME));
+ }
+ }
+
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/CompressionTree.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/CompressionTree.java
new file mode 100644
index 0000000000..d6baa48ee4
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/CompressionTree.java
@@ -0,0 +1,677 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+public class CompressionTree {
+
+ private String root;
+
+ private HashMap subject_layer;
+ private HashMap> predicate_tree_layer;
+
+ private HashMap>> adjacency_lists;
+
+ private int nb_triples;
+ private int initial_capacity;
+
+ private RDFTriple[] triples;
+
+ public CompressionTree(RDFTriple[] triples) {
+ nb_triples = triples.length;
+ initial_capacity = (int) ((0.2 * (double) nb_triples) / 0.75);
+ this.triples = triples;
+
+ adjacency_lists = new HashMap<>(initial_capacity);
+ subject_layer = new HashMap<>(initial_capacity);
+ predicate_tree_layer = new HashMap<>(initial_capacity);
+
+ generateAdjacencyLists();
+ generateHashedAdjacencyLists();
+ generateCompressionTree();
+ deleteAdjacencyLists();
+ }
+
+ public CompressionTree(RDFTriple[] triples, boolean empty) {
+ nb_triples = triples.length;
+ initial_capacity = (int) ((0.2 * (double) nb_triples) / 0.75);
+ this.triples = triples;
+
+ adjacency_lists = new HashMap<>(initial_capacity);
+ subject_layer = new HashMap<>(initial_capacity);
+ predicate_tree_layer = new HashMap<>(initial_capacity);
+
+ if (!empty) {
+ generateAdjacencyLists();
+ generateHashedAdjacencyLists();
+ generateCompressionTree();
+ deleteAdjacencyLists();
+ }
+ }
+
+ public void deleteAdjacencyLists() {
+ adjacency_lists = null;
+ }
+
+ public void generateCompressionTree() {
+ for (String subject: adjacency_lists.keySet()) {
+
+ HashMap> predicate_object_list_map = adjacency_lists.get(subject);
+
+ ArrayList predicateTrees = new ArrayList<>();
+
+ for (String predicate: predicate_object_list_map.keySet()) {
+ // Update predicate trees
+ ObjectList objectList = new ObjectList(predicate_object_list_map.get(predicate));
+ PredicateTree predicateTree = new PredicateTree(predicate, objectList);
+ predicateTrees.add(predicateTree);
+ }
+
+ predicate_tree_layer.put(subject, predicateTrees);
+
+ String subject_children_hash = getPredicateTreeDigest(predicate_tree_layer.get(subject));
+ subject_layer.put(subject, subject_children_hash);
+ }
+
+ updateRoot();
+ }
+
+ public void insertTriples(RDFTriple[] triples) {
+ for (int i = 0; i < triples.length; i++) {
+ insertTriple(triples[i]);
+ }
+ }
+
+ public void insertHashedTriples(RDFTriple[] triples) {
+ for (int i = 0; i < triples.length; i++) {
+ insertHashedTriple(triples[i]);
+ }
+ }
+
+ public void insertTriple(RDFTriple triple) {
+ insertTriple(triple.getSubject(), triple.getPredicate(), triple.getObject());
+ }
+
+ public void insertHashedTriple(RDFTriple hashed_triple) {
+ insertHashedTriple(hashed_triple.getSubject(), hashed_triple.getPredicate(), hashed_triple.getObject());
+ }
+
+ public void insertTriple(String subject, String predicate, String object) {
+ String hashed_subject = DigestUtils.sha256Hex(subject);
+ String hashed_predicate = DigestUtils.sha256Hex(predicate);
+ String hashed_object = DigestUtils.sha256Hex(object);
+
+ insert(hashed_subject, hashed_predicate, hashed_object);
+ }
+
+ public void insertHashedTriple(String hashed_subject, String hashed_predicate, String hashed_object) {
+ insert(hashed_subject, hashed_predicate, hashed_object);
+ }
+
+ private void insert(String hashed_subject, String hashed_predicate, String hashed_object) {
+ boolean subject_exists = subject_layer.containsKey(hashed_subject);
+
+ if (!subject_exists) {
+ ObjectList objectList = new ObjectList(hashed_object);
+ PredicateTree predicateTree = new PredicateTree(hashed_predicate, objectList);
+ ArrayList predicateTrees = new ArrayList<>();
+ predicateTrees.add(predicateTree);
+ predicate_tree_layer.put(hashed_subject, predicateTrees);
+ } else {
+ boolean predicate_exists = predicateExists(hashed_subject, hashed_predicate);
+
+ if (!predicate_exists) {
+ ObjectList objectList = new ObjectList(hashed_object);
+ PredicateTree predicateTree = new PredicateTree(hashed_predicate, objectList);
+ predicate_tree_layer.get(hashed_subject).add(predicateTree);
+ } else {
+ PredicateTree predicateTree = getPredicateTree(hashed_subject, hashed_predicate);
+ boolean object_exists = predicateTree.getObject_list().contains(hashed_object);
+
+ if (!object_exists) {
+ predicateTree.getObject_list().getList().add(hashed_object);
+ predicateTree.updateChildrenHash();
+ }
+ }
+ }
+
+ String predicate_tree_digest = getPredicateTreeDigest(predicate_tree_layer.get(hashed_subject));
+ subject_layer.put(hashed_subject, predicate_tree_digest);
+
+ updateRoot();
+ }
+
+ private PredicateTree getPredicateTree(String hashed_subject, String hashed_predicate) {
+ for (PredicateTree predicateTree: predicate_tree_layer.get(hashed_subject)) {
+ if (predicateTree.getPredicate().equals(hashed_predicate)) {
+ return predicateTree;
+ }
+ }
+
+ return null;
+ }
+
+ private boolean predicateExists(String hashed_subject, String hashed_predicate) {
+ for (PredicateTree predicateTree: predicate_tree_layer.get(hashed_subject)) {
+ if (predicateTree.getPredicate().equals(hashed_predicate)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void updateRoot() {
+ root = getDigest(subject_layer);
+ }
+
+ private String getPredicateTreeDigest(ArrayList predicateTrees) {
+ StringBuilder sb = new StringBuilder();
+
+ for (PredicateTree predicateTree: predicateTrees) {
+ sb.append(predicateTree.getDigest());
+ }
+
+ return DigestUtils.sha256Hex(sb.toString());
+ }
+
+ public void generateAdjacencyLists() {
+ generateAdjacencyLists(this.triples);
+ }
+
+ public void generateAdjacencyLists(RDFTriple[] triples) {
+ for (RDFTriple triple: triples) {
+ String subject = triple.getSubject();
+ String predicate = triple.getPredicate();
+ String object = triple.getObject();
+
+ HashMap> predicate_object_list_map = adjacency_lists.get(subject);
+
+ if (predicate_object_list_map == null) {
+ adjacency_lists.put(subject, createPredicateObjectListMap(predicate, object));
+ } else {
+ ArrayList object_list = predicate_object_list_map.get(predicate);
+
+ if (object_list == null) {
+ predicate_object_list_map.put(predicate, createObjectList(object));
+ } else {
+ boolean object_exists = object_list.contains(object);
+
+ if (!object_exists) {
+ object_list.add(object);
+ }
+ }
+ }
+ }
+ }
+
+ public void generateHashedAdjacencyLists() {
+ HashMap>> hashed_adjacency_list = new HashMap<>();
+
+ for (String subject: adjacency_lists.keySet()) {
+ HashMap> hashed_predicate_map = new HashMap<>();
+
+ for (String predicate: adjacency_lists.get(subject).keySet()) {
+ ArrayList hashed_object_list = new ArrayList<>();
+
+ for (String object: adjacency_lists.get(subject).get(predicate)) {
+ hashed_object_list.add(DigestUtils.sha256Hex(object));
+ }
+
+ hashed_predicate_map.put(DigestUtils.sha256Hex(predicate), hashed_object_list);
+ }
+
+ hashed_adjacency_list.put(DigestUtils.sha256Hex(subject), hashed_predicate_map);
+ }
+
+ adjacency_lists = hashed_adjacency_list;
+ }
+
+ private HashMap> createPredicateObjectListMap(String p, String o) {
+ ArrayList object_list = new ArrayList<>();
+ object_list.add(o);
+
+ HashMap> predicate_tree = new HashMap<>();
+ predicate_tree.put(p, object_list);
+
+ return predicate_tree;
+ }
+
+ private ArrayList createObjectList(String o) {
+ ArrayList object_list = new ArrayList<>();
+ object_list.add(o);
+ return object_list;
+ }
+
+ private String getDigest(HashMap map) {
+ StringBuilder sb = new StringBuilder();
+
+ for (String key: map.keySet()) {
+ String digest = DigestUtils.sha256Hex(key+map.get(key));
+ sb.append(digest);
+ }
+
+ return DigestUtils.sha256Hex(sb.toString());
+ }
+
+
+ private String getDigest(ArrayList list) {
+ StringBuilder sb = new StringBuilder();
+
+ for (String element : list) {
+ sb.append(element);
+ }
+
+ return DigestUtils.sha256Hex(sb.toString());
+ }
+
+ private String getDigest(Set set) {
+ StringBuilder sb = new StringBuilder();
+
+ for (String element : set) {
+ sb.append(element);
+ }
+
+ return DigestUtils.sha256Hex(sb.toString());
+ }
+
+ private String getHashTupleListDigest(ArrayList hashTuples) {
+ StringBuilder sb = new StringBuilder();
+
+ for (HashTuple hashTuple: hashTuples) {
+ String digest = DigestUtils.sha256Hex(hashTuple.getElementHash()+hashTuple.getChildrenHash());
+ sb.append(digest);
+ }
+
+ return DigestUtils.sha256Hex(sb.toString());
+ }
+
+ public int numberOfUniqueSubjects() {
+ return subject_layer.size();
+ }
+
+ public int numberOfUniquePredicates() {
+ int number_of_unique_predicates = 0;
+ for (String subject: predicate_tree_layer.keySet()) {
+ number_of_unique_predicates += predicate_tree_layer.get(subject).size();
+ }
+
+ return number_of_unique_predicates;
+ }
+
+ public int numberOfUniqueObjects() {
+ int number_of_unique_objects = 0;
+ for (String subject: predicate_tree_layer.keySet()) {
+ for (PredicateTree predicate_tree: predicate_tree_layer.get(subject)) {
+ number_of_unique_objects += predicate_tree.getObject_list().getList().size();
+ }
+ }
+ return number_of_unique_objects;
+ }
+
+ public float averageNumberOfPredicatesPerSubject() {
+ return (float) numberOfUniquePredicates()/ (float) numberOfUniqueSubjects();
+ }
+
+ public float averageNumberOfObjectsPerPredicate() {
+ return (float) numberOfUniqueObjects()/ (float) numberOfUniquePredicates();
+ }
+
+ public String getRoot() {
+ return root;
+ }
+
+ private boolean containsHashedTriple(RDFTriple triple) {
+ String subject = triple.getSubject();
+ String predicate = triple.getPredicate();
+ String object = triple.getObject();
+
+ boolean subject_exists = subject_layer.containsKey(subject);
+
+ if (subject_exists) {
+ ArrayList predicateTrees = predicate_tree_layer.get(subject);
+
+ for (PredicateTree predicateTree: predicateTrees) {
+ if (predicateTree.getPredicate().equals(predicate)) {
+ return predicateTree.getObject_list().getList().contains(object);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public Path getPathForTriple(RDFTriple triple) {
+ String subject = triple.getSubject();
+ String predicate = triple.getPredicate();
+ String object = triple.getObject();
+
+ RDFTriple hashed_triple = new RDFTriple(DigestUtils.sha256Hex(subject), DigestUtils.sha256Hex(predicate), DigestUtils.sha256Hex(object));
+ return getPath(hashed_triple);
+ }
+
+ public ArrayList get(String subject, String predicate, String object) {
+ if (subject == null && predicate == null && object == null) {
+ return null;
+ } else if (subject != null && predicate == null && object == null) {
+ return getTriplesUsingSubject(subject);
+ } else if (subject != null && predicate == null && object != null) {
+ return getTriplesUsingSubjectAndObject(subject, object);
+ } else if (subject != null && predicate != null && object == null) {
+ return getTriplesUsingSubjectAndPredicate(subject, predicate);
+ } else if (subject == null && predicate != null && object == null) {
+ return getTriplesUsingPredicate(predicate);
+ } else if (subject == null && predicate != null && object != null) {
+ return getTriplesUsingPredicateAndObject(predicate, object);
+ } else if (subject == null && predicate == null && object != null) {
+ return getTriplesUsingObject(object);
+ }
+
+ ArrayList triples = new ArrayList<>();
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples.add(triple);
+ return triples;
+ }
+
+ private ArrayList getTriplesUsingSubjectAndObject(String subject, String object) {
+ ArrayList predicateTrees = predicate_tree_layer.get(subject);
+
+ if (predicateTrees != null) {
+ ArrayList triples = new ArrayList<>();
+
+ for (PredicateTree predicateTree: predicateTrees) {
+ String predicate = predicateTree.getPredicate();
+
+ for (String current_object: predicateTree.getObject_list().getList()) {
+ if (current_object.equals(object)) {
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples.add(triple);
+ break;
+ }
+ }
+ }
+
+ return triples;
+ }
+
+ return null;
+ }
+
+ private ArrayList getTriplesUsingObject(String object) {
+ ArrayList triples = new ArrayList<>();
+
+ for (String subject: predicate_tree_layer.keySet()) {
+ for (PredicateTree predicateTree: predicate_tree_layer.get(subject)) {
+ for (String current_object: predicateTree.getObject_list().getList()) {
+ if (current_object.equals(object)) {
+ RDFTriple triple = new RDFTriple(subject, predicateTree.getPredicate(), object);
+ triples.add(triple);
+ break;
+ }
+ }
+ }
+ }
+
+ return triples;
+ }
+
+ private ArrayList getTriplesUsingPredicateAndObject(String predicate, String object) {
+ ArrayList triples = new ArrayList<>();
+
+ for (String subject: predicate_tree_layer.keySet()) {
+ for (PredicateTree predicateTree: predicate_tree_layer.get(subject)) {
+ String current_predicate = predicateTree.getPredicate();
+
+ if (current_predicate.equals(predicate)) {
+ for (String current_object: predicateTree.getObject_list().getList()) {
+ if (current_object.equals(object)) {
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples.add(triple);
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ return triples;
+ }
+
+ private ArrayList getTriplesUsingPredicate(String predicate) {
+ ArrayList triples = new ArrayList<>();
+
+ for (String subject: predicate_tree_layer.keySet()) {
+ for (PredicateTree predicateTree: predicate_tree_layer.get(subject)) {
+ String current_predicate = predicateTree.getPredicate();
+
+ if (current_predicate.equals(predicate)) {
+ for (String object: predicateTree.getObject_list().getList()) {
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples.add(triple);
+ }
+ break;
+ }
+ }
+ }
+
+ return triples;
+ }
+
+ private ArrayList getTriplesUsingSubjectAndPredicate(String subject, String predicate) {
+ ArrayList predicateTrees = predicate_tree_layer.get(subject);
+
+ if (predicateTrees != null) {
+ ArrayList triples = new ArrayList<>();
+
+ for (PredicateTree predicateTree: predicateTrees) {
+ String current_predicate = predicateTree.getPredicate();
+
+ if (predicate.equals(current_predicate)) {
+ for (String object : predicateTree.getObject_list().getList()) {
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples.add(triple);
+ }
+
+ return triples;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private ArrayList getTriplesUsingSubject(String subject) {
+ ArrayList predicateTrees = predicate_tree_layer.get(subject);
+
+ if (predicateTrees != null) {
+ ArrayList triples = new ArrayList<>();
+
+ for (PredicateTree predicateTree: predicateTrees) {
+ String predicate = predicateTree.getPredicate();
+
+ for (String object: predicateTree.getObject_list().getList()) {
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples.add(triple);
+ }
+ }
+
+ return triples;
+ }
+
+ return null;
+ }
+
+ public Path getPathForHashedTriple(RDFTriple triple) {
+ return getPath(triple);
+ }
+
+ private Path getPath(RDFTriple triple) {
+ String subject = triple.getSubject();
+ String predicate = triple.getPredicate();
+ String object = triple.getObject();
+
+ if (containsHashedTriple(triple)) {
+ ArrayList subject_list = new ArrayList<>(subject_layer.keySet());
+ ArrayList subject_children_hashes = new ArrayList<>(subject_layer.values());
+ int index_of_subject = subject_list.indexOf(subject);
+ subject_list.remove(subject);
+ subject_children_hashes.remove(index_of_subject);
+
+ ArrayList subject_siblings = new ArrayList<>();
+
+ for (int i = 0; i < subject_list.size(); i++) {
+ subject_siblings.add(new HashTuple(subject_list.get(i), subject_children_hashes.get(i)));
+ }
+
+ ArrayList predicate_list = new ArrayList<>();
+ ArrayList predicate_children_hashes = new ArrayList<>();
+ ArrayList object_list = null;
+
+ for (PredicateTree predicate_tree: predicate_tree_layer.get(subject)) {
+ predicate_list.add(predicate_tree.getPredicate());
+ predicate_children_hashes.add(predicate_tree.getChildren_hash());
+ if (predicate_tree.getPredicate().equals(predicate)) {
+ object_list = predicate_tree.getObject_list().getList();
+ }
+ }
+
+ int index_of_predicate = predicate_list.indexOf(predicate);
+ predicate_list.remove(predicate);
+ predicate_children_hashes.remove(index_of_predicate);
+
+ ArrayList predicate_siblings = new ArrayList<>();
+ for (int i = 0; i < predicate_list.size(); i++) {
+ predicate_siblings.add(new HashTuple(predicate_list.get(i), predicate_children_hashes.get(i)));
+ }
+
+ int index_of_object = object_list.indexOf(object);
+ object_list.remove(object);
+
+ return new Path(subject_siblings, predicate_siblings, object_list, index_of_subject, index_of_predicate, index_of_object);
+ }
+
+ return null;
+ }
+
+ public boolean doesTripleExist(RDFTriple triple, String root, Path path) {
+ String subject = triple.getSubject();
+ String predicate = triple.getPredicate();
+ String object = triple.getObject();
+
+ RDFTriple hashed_triple = new RDFTriple(DigestUtils.sha256Hex(subject), DigestUtils.sha256Hex(predicate), DigestUtils.sha256Hex(object));
+ return doesHashedTripleExist(hashed_triple, root, path);
+ }
+
+ public boolean doesHashedTripleExist(RDFTriple triple, String root, Path path) {
+ String subject = triple.getSubject();
+ String predicate = triple.getPredicate();
+ String object = triple.getObject();
+
+ ArrayList object_list = path.getObject_siblings();
+ ArrayList predicate_list = path.getPredicate_siblings();
+ ArrayList subject_list = path.getSubject_siblings();
+
+ object_list.add(path.getObject_index(), object);
+ String predicate_children_hash = getDigest(object_list);
+ predicate_list.add(path.getPredicate_index(), new HashTuple(predicate, predicate_children_hash));
+
+ String subject_children_hash = getHashTupleListDigest(predicate_list);
+ subject_list.add(path.getSubject_index(), new HashTuple(subject, subject_children_hash));
+
+ String reconstructed_root = getHashTupleListDigest(subject_list);
+
+ return root.equals(reconstructed_root);
+ }
+
+ public void printAdjacencyList() {
+ for (String subject: adjacency_lists.keySet()) {
+ System.out.println("Subject: " + subject);
+
+ for (String predicate: adjacency_lists.get(subject).keySet()) {
+ System.out.println("\t\tPredicate: " + predicate);
+
+ for (String object: adjacency_lists.get(subject).get(predicate)) {
+ System.out.println("\t\t\t\tObject: " + object);
+ }
+ }
+ }
+ }
+
+ public void printTree(int start, int end) {
+ if (start == -1 || start > subject_layer.keySet().size()) {
+ start = subject_layer.keySet().size()-1;
+ }
+
+ if (end == -1 || end > subject_layer.keySet().size()) {
+ end = subject_layer.keySet().size()-1;
+ }
+
+ int index = 0;
+ System.out.println("Root: " + root);
+ for (String subject: subject_layer.keySet()) {
+ if (index >= start) {
+ printSubjectTree(subject, index);
+ }
+ if (index == end) {
+ break;
+ }
+ index++;
+ }
+ }
+
+ public void printSubjectTree(String subject, int index) {
+ System.out.println("Subject " + index + ": (" + subject + ", " + subject_layer.get(subject) + ")");
+
+ for (int i = 0; i < predicate_tree_layer.get(subject).size(); i++) {
+ predicate_tree_layer.get(subject).get(i).print(2, i);
+ }
+ }
+
+ public void setRoot(String root) {
+ this.root = root;
+ }
+
+ public HashMap getSubject_layer() {
+ return subject_layer;
+ }
+
+ public void setSubject_layer(HashMap subject_layer) {
+ this.subject_layer = subject_layer;
+ }
+
+ public HashMap> getPredicate_tree_layer() {
+ return predicate_tree_layer;
+ }
+
+ public void setPredicate_tree_layer(HashMap> predicate_tree_layer) {
+ this.predicate_tree_layer = predicate_tree_layer;
+ }
+
+ public RDFTriple[] getTriples() {
+ return triples;
+ }
+
+ public double getSubjectToTriplesRatio() {
+ return (double) numberOfUniqueSubjects()/ (double) nb_triples;
+ }
+
+ public int getInitial_capacity() {
+ return initial_capacity;
+ }
+
+ public int getNumberOfRedundantTriples() {
+ int i = 9;
+
+ for (String subject: predicate_tree_layer.keySet()) {
+ for (PredicateTree predicate_tree: predicate_tree_layer.get(subject)) {
+ i+=predicate_tree.getObject_list().getList().size()-1;
+ }
+ }
+
+ return i;
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/HashTuple.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/HashTuple.java
new file mode 100644
index 0000000000..10d8747cd9
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/HashTuple.java
@@ -0,0 +1,35 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+public class HashTuple {
+ private String elementHash;
+ private String childrenHash;
+
+ public HashTuple(String elementHash, String childrenHash) {
+ this.elementHash = elementHash;
+ this.childrenHash = childrenHash;
+ }
+
+ public String getElementHash() {
+ return elementHash;
+ }
+
+ public void setElementHash(String elementHash) {
+ this.elementHash = elementHash;
+ }
+
+ public String getChildrenHash() {
+ return childrenHash;
+ }
+
+ public void setChildrenHash(String childrenHash) {
+ this.childrenHash = childrenHash;
+ }
+
+ @Override
+ public String toString() {
+ return "implementation.HashTuple{" +
+ "elementHash='" + elementHash + '\'' +
+ ", childrenHash='" + childrenHash + '\'' +
+ '}';
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/OWLToRDFTranslator.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/OWLToRDFTranslator.java
new file mode 100644
index 0000000000..997beffbb8
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/OWLToRDFTranslator.java
@@ -0,0 +1,95 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+import org.semanticweb.owlapi.formats.NTriplesDocumentFormat;
+import org.semanticweb.owlapi.model.OWLOntology;
+import org.semanticweb.owlapi.model.OWLOntologyStorageException;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+
+public class OWLToRDFTranslator {
+
+ private RDFTriple[] triples;
+
+ public OWLToRDFTranslator(OWLOntology ontology) throws OWLOntologyStorageException {
+ translate(ontology);
+ }
+
+ private void translate(OWLOntology ontology) throws OWLOntologyStorageException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ontology.saveOntology(new NTriplesDocumentFormat(), out);
+
+ byte[] buf = out.toByteArray();
+ char[] file = byteArrayToCharArray(buf);
+ ArrayList lines = removeComments(getLines(file));
+
+ this.triples = linesToTriples(lines);
+ }
+
+ private ArrayList getLines(char[] file) {
+ ArrayList lines = new ArrayList<>();
+ String file_as_string = String.copyValueOf(file);
+ StringTokenizer tokenizer = new StringTokenizer(file_as_string, "\n");
+ while (tokenizer.hasMoreTokens()) {
+ lines.add(tokenizer.nextToken());
+ }
+ return lines;
+ }
+
+ private ArrayList removeComments(ArrayList lines) {
+ ArrayList linesNoComments = new ArrayList<>();
+
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ if (line.charAt(0) != '#') {
+ linesNoComments.add(line);
+ }
+ }
+
+ return linesNoComments;
+ }
+
+
+ private char[] byteArrayToCharArray(byte[] buf) {
+ char[] arr = new char[buf.length];
+ for (int i = 0; i < buf.length; i++) {
+ arr[i] = (char) buf[i];
+ }
+ return arr;
+ }
+
+ private RDFTriple[] linesToTriples(ArrayList lines) {
+ RDFTriple[] triples = new RDFTriple[lines.size()];
+
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ StringTokenizer tokenizer = new StringTokenizer(line, " ");
+ String subject = tokenizer.nextToken();
+ String predicate = tokenizer.nextToken();
+
+ // Compute the "object" as the rest of the string (without including the '.' at the end)
+ StringBuilder sb = new StringBuilder();
+
+ while (tokenizer.hasMoreTokens()) {
+ sb.append(tokenizer.nextToken());
+ }
+
+ sb.deleteCharAt(sb.length()-1);
+ String object = sb.toString();
+
+ RDFTriple triple = new RDFTriple(subject, predicate, object);
+ triples[i] = triple;
+ }
+
+ return triples;
+ }
+
+ public RDFTriple[] getTriples() {
+ return triples;
+ }
+
+ public void setTriples(RDFTriple[] triples) {
+ this.triples = triples;
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/ObjectList.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/ObjectList.java
new file mode 100644
index 0000000000..e9cc3d6d75
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/ObjectList.java
@@ -0,0 +1,56 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.util.ArrayList;
+
+public class ObjectList {
+ private ArrayList list;
+
+
+ public ObjectList(ArrayList list) {
+ this.list = list;
+ }
+
+ public ObjectList(String object) {
+ ArrayList object_list = new ArrayList<>();
+ object_list.add(object);
+ this.list = object_list;
+ }
+
+ public String getDigest() {
+ StringBuilder sb = new StringBuilder();
+
+ for (String element : list) {
+ sb.append(element);
+ }
+
+ return DigestUtils.sha256Hex(sb.toString());
+ }
+
+ public ArrayList getList() {
+ return list;
+ }
+
+ public void setList(ArrayList list) {
+ this.list = list;
+ }
+
+ public boolean contains(String object) {
+ return list.contains(object);
+ }
+
+ private String getIndentedString(int nb_indents) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("\t".repeat(Math.max(0, nb_indents)));
+
+ return sb.toString();
+ }
+
+ public void print(int nb_indents) {
+ for (int i = 0; i < list.size(); i++) {
+ System.out.println(getIndentedString(nb_indents) + "Object " + i + ": " + list.get(i));
+ }
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/Path.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/Path.java
new file mode 100644
index 0000000000..2253062127
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/Path.java
@@ -0,0 +1,59 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+import java.util.ArrayList;
+
+public class Path {
+
+ private ArrayList subject_siblings;
+ private ArrayList predicate_siblings;
+ private ArrayList object_siblings;
+
+ private int subject_index;
+ private int predicate_index;
+ private int object_index;
+
+ public Path(ArrayList subject_siblings, ArrayList predicate_siblings, ArrayList object_siblings, int subject_index, int predicate_index, int object_index) {
+ this.subject_siblings = subject_siblings;
+ this.predicate_siblings = predicate_siblings;
+ this.object_siblings = object_siblings;
+ this.subject_index = subject_index;
+ this.predicate_index = predicate_index;
+ this.object_index = object_index;
+ }
+
+ public ArrayList getObject_siblings() {
+ return object_siblings;
+ }
+
+ public int getObject_index() {
+ return object_index;
+ }
+
+ public int getPredicate_index() {
+ return predicate_index;
+ }
+
+ public int getSubject_index() {
+ return subject_index;
+ }
+
+ public ArrayList getSubject_siblings() {
+ return subject_siblings;
+ }
+
+ public ArrayList getPredicate_siblings() {
+ return predicate_siblings;
+ }
+
+ @Override
+ public String toString() {
+ return "implementation.Path{" +
+ "subject_siblings=" + subject_siblings +
+ ", predicate_siblings=" + predicate_siblings +
+ ", object_siblings=" + object_siblings +
+ ", subject_index=" + subject_index +
+ ", predicate_index=" + predicate_index +
+ ", object_index=" + object_index +
+ '}';
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/PredicateTree.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/PredicateTree.java
new file mode 100644
index 0000000000..afefccd052
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/PredicateTree.java
@@ -0,0 +1,60 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+public class PredicateTree {
+ private String predicate;
+ private String children_hash;
+ private ObjectList object_list;
+
+ public PredicateTree(String predicate, ObjectList object_list) {
+ this.predicate = predicate;
+ this.children_hash = object_list.getDigest();
+ this.object_list = object_list;
+ }
+
+ public void updateChildrenHash() {
+ children_hash = object_list.getDigest();
+ }
+
+ public String getDigest() {
+ return DigestUtils.sha256Hex(predicate+children_hash);
+ }
+
+ public String getPredicate() {
+ return predicate;
+ }
+
+ public void setPredicate(String predicate) {
+ this.predicate = predicate;
+ }
+
+ public String getChildren_hash() {
+ return children_hash;
+ }
+
+ public void setChildren_hash(String children_hash) {
+ this.children_hash = children_hash;
+ }
+
+ public ObjectList getObject_list() {
+ return object_list;
+ }
+
+ public void setObject_list(ObjectList object_list) {
+ this.object_list = object_list;
+ }
+
+ private String getIndentationString(int nb_indents) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("\t".repeat(Math.max(0, nb_indents)));
+
+ return sb.toString();
+ }
+
+ public void print(int nb_indents, int index) {
+ System.out.println(getIndentationString(nb_indents) + "Predicate " + index + ": (" + predicate + ", " + children_hash + ")");
+ object_list.print(nb_indents+2);
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/RDFTriple.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/RDFTriple.java
new file mode 100644
index 0000000000..dd4e4b7021
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/compression/tree/RDFTriple.java
@@ -0,0 +1,46 @@
+package ch.unifr.digits.webprotege.attestation.server.compression.tree;
+
+public class RDFTriple {
+ private String subject;
+ private String predicate;
+ private String object;
+
+ public RDFTriple(String subject, String predicate, String object) {
+ this.subject = subject;
+ this.predicate = predicate;
+ this.object = object;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getPredicate() {
+ return predicate;
+ }
+
+ public void setPredicate(String predicate) {
+ this.predicate = predicate;
+ }
+
+ public String getObject() {
+ return object;
+ }
+
+ public void setObject(String object) {
+ this.object = object;
+ }
+
+ @Override
+ public String toString() {
+ return "implementation.RDFTriple{" +
+ "subject='" + subject + '\'' +
+ ", predicate='" + predicate + '\'' +
+ ", object='" + object + '\'' +
+ '}';
+ }
+}
diff --git a/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/contracts/ChangeTracking.java b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/contracts/ChangeTracking.java
new file mode 100644
index 0000000000..ad499961da
--- /dev/null
+++ b/webprotege-attestation-lib/src/main/java/ch/unifr/digits/webprotege/attestation/server/contracts/ChangeTracking.java
@@ -0,0 +1,132 @@
+package ch.unifr.digits.webprotege.attestation.server.contracts;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.web3j.abi.TypeReference;
+import org.web3j.abi.datatypes.Address;
+import org.web3j.abi.datatypes.Bool;
+import org.web3j.abi.datatypes.Function;
+import org.web3j.abi.datatypes.Type;
+import org.web3j.abi.datatypes.Utf8String;
+import org.web3j.abi.datatypes.generated.Uint256;
+import org.web3j.crypto.Credentials;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.RemoteFunctionCall;
+import org.web3j.protocol.core.methods.response.TransactionReceipt;
+import org.web3j.tuples.generated.Tuple4;
+import org.web3j.tx.Contract;
+import org.web3j.tx.TransactionManager;
+import org.web3j.tx.gas.ContractGasProvider;
+
+/**
+ * Auto generated code.
+ *
Do not modify!
+ *
Please use the web3j command line tools,
+ * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the
+ * codegen module to update.
+ *
+ *
Generated with web3j version 4.7.0.
+ */
+@SuppressWarnings("rawtypes")
+public class ChangeTracking extends Contract {
+ public static final String BINARY = "Bin file was not provided";
+
+ public static final String FUNC_ATTEST = "attest";
+
+ public static final String FUNC_VERIFY = "verify";
+
+ public static final String FUNC_VERIFYENTITY = "verifyEntity";
+
+ @Deprecated
+ protected ChangeTracking(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
+ super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
+ }
+
+ protected ChangeTracking(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
+ super(BINARY, contractAddress, web3j, credentials, contractGasProvider);
+ }
+
+ @Deprecated
+ protected ChangeTracking(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
+ super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
+ }
+
+ protected ChangeTracking(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
+ super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider);
+ }
+
+ public RemoteFunctionCall attest(String ontologyIri, String versionIri, String name, String hash, List classHashes) {
+ final Function function = new Function(
+ FUNC_ATTEST,
+ Arrays.asList(new org.web3j.abi.datatypes.Utf8String(ontologyIri),
+ new org.web3j.abi.datatypes.Utf8String(versionIri),
+ new org.web3j.abi.datatypes.Utf8String(name),
+ new org.web3j.abi.datatypes.Utf8String(hash),
+ new org.web3j.abi.datatypes.DynamicArray(
+ org.web3j.abi.datatypes.generated.Int256.class,
+ org.web3j.abi.Utils.typeMap(classHashes, org.web3j.abi.datatypes.generated.Int256.class))),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ public RemoteFunctionCall> verify(String ontologyIri, String versionIri, String hash) {
+ final Function function = new Function(FUNC_VERIFY,
+ Arrays.asList(new org.web3j.abi.datatypes.Utf8String(ontologyIri),
+ new org.web3j.abi.datatypes.Utf8String(versionIri),
+ new org.web3j.abi.datatypes.Utf8String(hash)),
+ Arrays.>asList(new TypeReference() {}, new TypeReference() {}, new TypeReference() {}, new TypeReference() {}));
+ return new RemoteFunctionCall>(function,
+ new Callable>() {
+ @Override
+ public Tuple4 call() throws Exception {
+ List results = executeCallMultipleValueReturn(function);
+ return new Tuple4(
+ (Boolean) results.get(0).getValue(),
+ (String) results.get(1).getValue(),
+ (String) results.get(2).getValue(),
+ (BigInteger) results.get(3).getValue());
+ }
+ });
+ }
+
+ public RemoteFunctionCall> verifyEntity(String ontologyIri, String versionIri, BigInteger entityHash) {
+ final Function function = new Function(FUNC_VERIFYENTITY,
+ Arrays.asList(new org.web3j.abi.datatypes.Utf8String(ontologyIri),
+ new org.web3j.abi.datatypes.Utf8String(versionIri),
+ new org.web3j.abi.datatypes.generated.Int256(entityHash)),
+ Arrays.>asList(new TypeReference() {}, new TypeReference() {}, new TypeReference() {}, new TypeReference() {}));
+ return new RemoteFunctionCall>(function,
+ new Callable>() {
+ @Override
+ public Tuple4 call() throws Exception {
+ List results = executeCallMultipleValueReturn(function);
+ return new Tuple4