• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Nanopublication / nanopub-java / 25663196917

11 May 2026 09:55AM UTC coverage: 53.534% (+0.1%) from 53.432%
25663196917

Pull #82

github

web-flow
Merge 40ef96265 into fa18b96da
Pull Request #82: feat(server): gate registry instances on Nanopub-Registry-Status

1271 of 3286 branches covered (38.68%)

Branch coverage included in aggregate %.

5651 of 9644 relevant lines covered (58.6%)

8.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.26
src/main/java/org/nanopub/extra/server/NanopubServerUtils.java
1
package org.nanopub.extra.server;
2

3
import org.apache.http.Header;
4
import org.apache.http.HttpResponse;
5
import org.eclipse.rdf4j.common.exception.RDF4JException;
6
import org.eclipse.rdf4j.model.IRI;
7
import org.eclipse.rdf4j.model.Statement;
8
import org.eclipse.rdf4j.model.vocabulary.RDF;
9
import org.nanopub.MalformedNanopubException;
10
import org.nanopub.Nanopub;
11
import org.nanopub.extra.services.ServiceLookup;
12
import org.nanopub.extra.setting.NanopubSetting;
13
import org.nanopub.vocabulary.NPS;
14
import org.nanopub.vocabulary.NPX;
15
import org.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17

18
import java.io.IOException;
19
import java.util.ArrayList;
20
import java.util.Date;
21
import java.util.LinkedHashSet;
22
import java.util.List;
23
import java.util.Locale;
24
import java.util.concurrent.ConcurrentHashMap;
25
import java.util.concurrent.ConcurrentMap;
26

27
/**
28
 * Utility class for handling nanopub server-related operations.
29
 */
30
public class NanopubServerUtils {
31

32
    private static final Logger logger = LoggerFactory.getLogger(NanopubServerUtils.class);
9✔
33

34
    /**
35
     * System property naming a whitespace-separated list of registry server URLs.
36
     * When set, this overrides discovery via the nanopub setting (env var
37
     * {@code NANOPUB_REGISTRY_INSTANCES} also accepted).
38
     */
39
    public static final String REGISTRY_INSTANCES_PROPERTY = "nanopub.registry.instances";
40

41
    /**
42
     * Environment variable equivalent of {@link #REGISTRY_INSTANCES_PROPERTY}.
43
     */
44
    public static final String REGISTRY_INSTANCES_ENV = "NANOPUB_REGISTRY_INSTANCES";
45

46
    /**
47
     * Singleton, no instances.
48
     */
49
    private NanopubServerUtils() {
50
    }
51

52
    private static final List<String> bootstrapServerList = new ArrayList<>();
12✔
53
    private static List<String> registryServerList;
54

55
    /**
56
     * Returns a list of bootstrap servers.
57
     *
58
     * @return a list of bootstrap server URIs
59
     */
60
    public static List<String> getBootstrapServerList() {
61
        if (bootstrapServerList.isEmpty()) {
9✔
62
            try {
63
                for (IRI iri : NanopubSetting.getDefaultSetting().getBootstrapServices()) {
33✔
64
                    bootstrapServerList.add(iri.stringValue());
15✔
65
                }
3✔
66
            } catch (RDF4JException | MalformedNanopubException | IOException ex) {
×
67
                throw new RuntimeException(ex);
×
68
            }
3✔
69
        }
70
        return bootstrapServerList;
6✔
71
    }
72

73
    /**
74
     * Returns the list of registry server URLs to seed a {@link ServerIterator}.
75
     * <p>
76
     * Sources, in order of priority:
77
     * <ol>
78
     *   <li>{@code nanopub.registry.instances} system property /
79
     *       {@code NANOPUB_REGISTRY_INSTANCES} env var (whitespace-separated URLs).
80
     *       Replaces both bootstrap and discovery when set.</li>
81
     *   <li>Otherwise: the union of the setting's bootstrap services and
82
     *       {@link ServiceLookup#getServices(IRI)} for {@link NPS#NANOPUB_REGISTRY_1_0}.
83
     *       Bootstrap servers come first; discovered servers are appended without duplicates.</li>
84
     * </ol>
85
     * Cached for the JVM lifetime.
86
     *
87
     * @return a list of registry server URLs
88
     */
89
    public static synchronized List<String> getRegistryServerList() {
90
        if (registryServerList != null) return registryServerList;
12✔
91
        String override = System.getProperty(REGISTRY_INSTANCES_PROPERTY);
9✔
92
        if (override == null || override.isEmpty()) override = System.getenv(REGISTRY_INSTANCES_ENV);
24!
93
        if (override != null && !override.trim().isEmpty()) {
18!
94
            List<String> list = new ArrayList<>();
12✔
95
            for (String url : override.trim().split("\\s+")) list.add(url);
69✔
96
            logger.info("Using {} registry instance(s) from override", list.size());
18✔
97
            registryServerList = list;
6✔
98
            return registryServerList;
6✔
99
        }
100
        List<String> bootstrap = getBootstrapServerList();
6✔
101
        List<String> discovered = ServiceLookup.getServices(NPS.NANOPUB_REGISTRY_1_0);
9✔
102
        LinkedHashSet<String> union = new LinkedHashSet<>(bootstrap);
15✔
103
        union.addAll(discovered);
12✔
104
        logger.info("Using {} registry instance(s): {} bootstrap + {} discovered",
24✔
105
                union.size(), bootstrap.size(), discovered.size());
45✔
106
        registryServerList = new ArrayList<>(union);
15✔
107
        return registryServerList;
6✔
108
    }
109

110
    /**
111
     * HTTP response header carrying the registry instance's sync state.
112
     * See nanopub-registry's {@code Page} / {@code RegistryInfo.status}.
113
     */
114
    public static final String REGISTRY_STATUS_HEADER = "Nanopub-Registry-Status";
115

116
    /**
117
     * System property setting the cool-down (in seconds) before a registry
118
     * instance evicted for non-ready status is re-considered. Default
119
     * {@value #DEFAULT_REGISTRY_EVICTION_COOLDOWN_SECONDS}. Env var
120
     * {@code NANOPUB_REGISTRY_EVICTION_COOLDOWN_SECONDS} also accepted.
121
     */
122
    public static final String REGISTRY_EVICTION_COOLDOWN_PROPERTY = "nanopub.registry.eviction-cooldown-seconds";
123

124
    /**
125
     * Environment variable equivalent of {@link #REGISTRY_EVICTION_COOLDOWN_PROPERTY}.
126
     */
127
    public static final String REGISTRY_EVICTION_COOLDOWN_ENV = "NANOPUB_REGISTRY_EVICTION_COOLDOWN_SECONDS";
128

129
    private static final int DEFAULT_REGISTRY_EVICTION_COOLDOWN_SECONDS = 300;
130

131
    private static final ConcurrentMap<String, Long> evictedRegistriesUntil = new ConcurrentHashMap<>();
15✔
132

133
    /**
134
     * Returns the registry eviction cool-down in milliseconds, resolved from
135
     * {@link #REGISTRY_EVICTION_COOLDOWN_PROPERTY}, {@link #REGISTRY_EVICTION_COOLDOWN_ENV},
136
     * or the default of {@value #DEFAULT_REGISTRY_EVICTION_COOLDOWN_SECONDS} seconds.
137
     */
138
    public static long getRegistryEvictionCooldownMillis() {
139
        String value = System.getProperty(REGISTRY_EVICTION_COOLDOWN_PROPERTY);
9✔
140
        if (value == null || value.isEmpty()) value = System.getenv(REGISTRY_EVICTION_COOLDOWN_ENV);
24!
141
        if (value != null && !value.trim().isEmpty()) {
18!
142
            try {
143
                long n = Long.parseLong(value.trim());
12✔
144
                if (n >= 0) return n * 1000L;
24✔
145
                logger.warn("Ignoring {}={}: must be >= 0", REGISTRY_EVICTION_COOLDOWN_PROPERTY, value);
15✔
146
            } catch (NumberFormatException ex) {
3✔
147
                logger.warn("Ignoring {}={}: not a number", REGISTRY_EVICTION_COOLDOWN_PROPERTY, value);
15✔
148
            }
3✔
149
        }
150
        return DEFAULT_REGISTRY_EVICTION_COOLDOWN_SECONDS * 1000L;
6✔
151
    }
152

153
    /**
154
     * Returns true if the given registry status signals a fully-loaded state
155
     * usable for fetching nanopubs ({@code ready} or {@code updating};
156
     * case-insensitive). {@code updating} is the transient state entered from
157
     * {@code ready} during the registry's periodic re-sync, so the corpus is
158
     * still complete. {@code coreReady} is rejected: at that stage only core
159
     * nanopubs are loaded and the rest are still being fetched — matching the
160
     * registry's own peer-sync checks in {@code RegistryPeerConnector} and
161
     * {@code NanopubLoader}. Null/empty is treated as ready for backwards
162
     * compatibility with older registry instances that do not report a status.
163
     */
164
    public static boolean isReadyRegistryStatus(String status) {
165
        if (status == null || status.isEmpty()) return true;
21✔
166
        String lower = status.toLowerCase(Locale.ROOT);
12✔
167
        return lower.equals("ready") || lower.equals("updating");
36✔
168
    }
169

170
    /**
171
     * Returns true if the response's {@link #REGISTRY_STATUS_HEADER} signals a
172
     * fully-loaded state. Missing header is treated as ready (older instances).
173
     */
174
    public static boolean isReadyRegistryStatus(HttpResponse resp) {
175
        Header h = resp.getFirstHeader(REGISTRY_STATUS_HEADER);
12✔
176
        return isReadyRegistryStatus(h == null ? null : h.getValue());
24✔
177
    }
178

179
    /**
180
     * Marks the given registry URL as evicted for {@link #getRegistryEvictionCooldownMillis()}.
181
     */
182
    public static void evictRegistry(String registryUrl, String reason) {
183
        long until = System.currentTimeMillis() + getRegistryEvictionCooldownMillis();
12✔
184
        evictedRegistriesUntil.put(registryUrl, until);
18✔
185
        logger.warn("Evicting Nanopub Registry {} until {} ({})", registryUrl, new Date(until), reason);
60✔
186
    }
3✔
187

188
    /**
189
     * Returns true if the given registry URL is currently evicted (cool-down active).
190
     */
191
    public static boolean isRegistryEvicted(String registryUrl) {
192
        Long until = evictedRegistriesUntil.get(registryUrl);
15✔
193
        return until != null && until > System.currentTimeMillis();
33✔
194
    }
195

196
    /**
197
     * Checks if the given nanopub is a protected nanopub.
198
     *
199
     * @param np the nanopub to check
200
     * @return true if the nanopub is protected, false otherwise
201
     */
202
    public static boolean isProtectedNanopub(Nanopub np) {
203
        for (Statement st : np.getPubinfo()) {
33✔
204
            if (!st.getSubject().equals(np.getUri())) continue;
18!
205
            if (!st.getPredicate().equals(RDF.TYPE)) continue;
18✔
206
            if (st.getObject().equals(NPX.PROTECTED_NANOPUB)) return true;
21!
207
        }
×
208
        return false;
6✔
209
    }
210

211
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc