diff --git a/src/fr/devinsy/statoolinfos/core/Configuration.java b/src/fr/devinsy/statoolinfos/core/Configuration.java index 21a32c9..7e3b678 100644 --- a/src/fr/devinsy/statoolinfos/core/Configuration.java +++ b/src/fr/devinsy/statoolinfos/core/Configuration.java @@ -189,7 +189,7 @@ public class Configuration extends PathPropertyList * * @return the crawl input */ - public File getCrawlInput() + public File getCrawlInputFile() { File result; diff --git a/src/fr/devinsy/statoolinfos/core/Factory.java b/src/fr/devinsy/statoolinfos/core/Factory.java index 97ab525..db150c1 100644 --- a/src/fr/devinsy/statoolinfos/core/Factory.java +++ b/src/fr/devinsy/statoolinfos/core/Factory.java @@ -84,16 +84,15 @@ public class Factory PathProperties properties = PathPropertyUtils.load(federationFile); result = new Federation(properties); - result.setLocalFile(federationFile); + result.setInputFile(federationFile); PathProperties subs = result.getByPrefix("subs"); for (PathProperty property : subs) { if (StringUtils.startsWith(property.getValue(), "http")) { - File subFile = cache.restoreFile(new URL(property.getValue())); - Organization organization = loadOrganization(subFile, cache); - organization.setFederation(result); + URL inputURL = new URL(property.getValue()); + Organization organization = loadOrganization(inputURL, cache); result.getOrganizations().add(organization); } } @@ -119,15 +118,53 @@ public class Factory PathProperties properties = PathPropertyUtils.load(organizationFile); result = new Organization(properties); - result.setLocalFile(organizationFile); + result.setInputFile(organizationFile); PathProperties subs = result.getByPrefix("subs"); for (PathProperty property : subs) { if (StringUtils.startsWith(property.getValue(), "http")) { - File subFile = cache.restoreFile(new URL(property.getValue())); - Service service = loadService(subFile); + URL serviceInputFile = new URL(property.getValue()); + Service service = loadService(serviceInputFile, cache); + service.setOrganization(result); + result.getServices().add(service); + } + } + + // + return result; + } + + /** + * Load organization. + * + * @param inputURL + * the input + * @param cache + * the cache + * @return the organization + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public static Organization loadOrganization(final URL inputURL, final CrawlCache cache) throws IOException + { + Organization result; + + File inputFile = cache.restoreFile(inputURL); + + PathProperties properties = PathPropertyUtils.load(inputFile); + result = new Organization(properties); + result.setInputFile(inputFile); + result.setInputURL(inputURL); + + PathProperties subs = result.getByPrefix("subs"); + for (PathProperty property : subs) + { + if (StringUtils.startsWith(property.getValue(), "http")) + { + URL serviceInputURL = new URL(property.getValue()); + Service service = loadService(serviceInputURL, cache); service.setOrganization(result); result.getServices().add(service); } @@ -143,13 +180,16 @@ public class Factory * @return the service * @throws IOException */ - public static Service loadService(final File serviceFile) throws IOException + public static Service loadService(final URL inputURL, final CrawlCache cache) throws IOException { Service result; - PathProperties properties = PathPropertyUtils.load(serviceFile); + File inputFile = cache.restoreFile(inputURL); + + PathProperties properties = PathPropertyUtils.load(inputFile); result = new Service(properties); - result.setLocalFile(serviceFile); + result.setInputFile(inputFile); + result.setInputURL(inputURL); // return result; diff --git a/src/fr/devinsy/statoolinfos/core/Federation.java b/src/fr/devinsy/statoolinfos/core/Federation.java index 01ae2f4..be1d945 100644 --- a/src/fr/devinsy/statoolinfos/core/Federation.java +++ b/src/fr/devinsy/statoolinfos/core/Federation.java @@ -34,7 +34,7 @@ public class Federation extends PathPropertyList { private static final long serialVersionUID = -8970835291634661580L; private Organizations organizations; - private File localFile; + private File inputFile; /** * Instantiates a new federation. @@ -100,14 +100,9 @@ public class Federation extends PathPropertyList return result; } - /** - * Gets the local file. - * - * @return the local file - */ - public File getLocalFile() + public File getInputFile() { - return this.localFile; + return this.inputFile; } /** @@ -203,8 +198,8 @@ public class Federation extends PathPropertyList return result; } - public void setLocalFile(final File localFile) + public void setInputFile(final File inputFile) { - this.localFile = localFile; + this.inputFile = inputFile; } } diff --git a/src/fr/devinsy/statoolinfos/core/Organization.java b/src/fr/devinsy/statoolinfos/core/Organization.java index d311cab..508a374 100644 --- a/src/fr/devinsy/statoolinfos/core/Organization.java +++ b/src/fr/devinsy/statoolinfos/core/Organization.java @@ -35,7 +35,8 @@ public class Organization extends PathPropertyList private static final long serialVersionUID = -2709210934548224213L; private Federation federation; private Services services; - private File localFile; + private File inputFile; + private URL inputURL; /** * Instantiates a new organization. @@ -81,9 +82,24 @@ public class Organization extends PathPropertyList return this.federation; } - public File getLocalFile() + public File getInputFile() { - return this.localFile; + return this.inputFile; + } + + /** + * Gets the crawled input URL. + * + * @return the crawled input URL + */ + public URL getInputURL() + { + URL result; + + result = this.inputURL; + + // + return result; } /** @@ -181,8 +197,13 @@ public class Organization extends PathPropertyList this.federation = federation; } - public void setLocalFile(final File localFile) + public void setInputFile(final File inputFile) { - this.localFile = localFile; + this.inputFile = inputFile; + } + + public void setInputURL(final URL inputURL) + { + this.inputURL = inputURL; } } diff --git a/src/fr/devinsy/statoolinfos/core/Service.java b/src/fr/devinsy/statoolinfos/core/Service.java index 1cfc8be..e746bc2 100644 --- a/src/fr/devinsy/statoolinfos/core/Service.java +++ b/src/fr/devinsy/statoolinfos/core/Service.java @@ -34,7 +34,8 @@ public class Service extends PathPropertyList { private static final long serialVersionUID = 3629841771102288863L; private Organization organization; - private File localFile; + private File inputFile; + private URL inputURL; /** * Instantiates a new service. @@ -70,9 +71,14 @@ public class Service extends PathPropertyList return result; } - public File getLocalFile() + public File getInputFile() { - return this.localFile; + return this.inputFile; + } + + public URL getInputURL() + { + return this.inputURL; } /** @@ -160,9 +166,14 @@ public class Service extends PathPropertyList return result; } - public void setLocalFile(final File localFile) + public void setInputFile(final File inputFile) { - this.localFile = localFile; + this.inputFile = inputFile; + } + + public void setInputURL(final URL inputURL) + { + this.inputURL = inputURL; } public void setOrganization(final Organization organization) diff --git a/src/fr/devinsy/statoolinfos/core/StatoolInfosUtils.java b/src/fr/devinsy/statoolinfos/core/StatoolInfosUtils.java index e626900..c49a2f6 100644 --- a/src/fr/devinsy/statoolinfos/core/StatoolInfosUtils.java +++ b/src/fr/devinsy/statoolinfos/core/StatoolInfosUtils.java @@ -20,9 +20,12 @@ package fr.devinsy.statoolinfos.core; import java.io.File; import java.io.IOException; +import java.net.HttpURLConnection; import java.net.URL; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Iterator; @@ -102,6 +105,42 @@ public class StatoolInfosUtils FileUtils.copyURLToFile(url, finalTarget); } + /** + * Epoch to local date time. + * + * @param epoch + * the epoch + * @return the local date time + */ + public static LocalDateTime epochToLocalDateTime(final long epoch) + { + LocalDateTime result; + + result = Instant.ofEpochMilli(epoch).atZone(ZoneId.systemDefault()).toLocalDateTime(); + + // + return result; + } + + /** + * File local date time. + * + * @param file + * the file + * @return the local date time + */ + public static LocalDateTime fileLocalDateTime(final File file) + { + LocalDateTime result; + + long epoch = file.lastModified(); + + result = epochToLocalDateTime(epoch); + + // + return result; + } + /** * Generate cat logo. * @@ -417,4 +456,27 @@ public class StatoolInfosUtils { } } + + /** + * Url last modified. + * + * @param url + * the url + * @return the local date time + * @throws IOException + */ + public static LocalDateTime urlLastModified(final URL url) throws IOException + { + LocalDateTime result; + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + Long epoch = connection.getLastModified(); + connection.disconnect(); + result = epochToLocalDateTime(epoch); + // result = + // Instant.ofEpochMilli(epoch).atZone(ZoneId.of("GMT")).toLocalDateTime(); + + // + return result; + } } diff --git a/src/fr/devinsy/statoolinfos/crawl/CrawlCache.java b/src/fr/devinsy/statoolinfos/crawl/CrawlCache.java index 92f1ecc..e16714c 100644 --- a/src/fr/devinsy/statoolinfos/crawl/CrawlCache.java +++ b/src/fr/devinsy/statoolinfos/crawl/CrawlCache.java @@ -107,21 +107,21 @@ public class CrawlCache /** * Restore file. * - * @param url - * the url + * @param key + * the key * @return the file */ - public File restoreFile(final URL url) + public File restoreFile(final String key) { File result; - if (url == null) + if (StringUtils.isBlank(key)) { throw new IllegalArgumentException("Null parameter."); } else { - result = buildFile(url.toString()); + result = buildFile(key); if (!result.exists()) { result = null; @@ -132,6 +132,42 @@ public class CrawlCache return result; } + /** + * Restore file. + * + * @param url + * the url + * @return the file + */ + public File restoreFile(final URL url) + { + File result; + + result = restoreFile(url.toString()); + + // + return result; + } + + /** + * Restore file to. + * + * @param key + * the key + * @param target + * the target + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public void restoreFileTo(final String key, final File target) throws IOException + { + File logoFile = restoreFile(key); + if (logoFile != null) + { + FileUtils.copyFile(logoFile, target); + } + } + /** * Restore file to. * @@ -144,14 +180,7 @@ public class CrawlCache */ public void restoreFileTo(final URL url, final File target) throws IOException { - if (url != null) - { - File logoFile = restoreFile(url); - if (logoFile != null) - { - FileUtils.copyFile(logoFile, target); - } - } + restoreFile(url.toString()); } /** @@ -238,6 +267,33 @@ public class CrawlCache return result; } + /** + * Store. + * + * @param key + * the key + * @return the file + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public File store(final String key, final File source) throws IOException + { + File result; + + if (StringUtils.isBlank(key)) + { + result = null; + } + else + { + result = buildFile(key); + FileUtils.copyFile(source, result); + } + + // + return result; + } + /** * Store. * @@ -265,6 +321,36 @@ public class CrawlCache return result; } + /** + * Store properties. + * + * @param key + * the key + * @param properties + * the properties + * @return the file + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public File storeProperties(final String key, final PathProperties properties) throws IOException + { + File result; + + if (StringUtils.isBlank(key)) + { + result = null; + } + else + { + result = buildFile(key); + + PathPropertyUtils.save(result, properties); + } + + // + return result; + } + /** * Store. * @@ -286,8 +372,7 @@ public class CrawlCache } else { - String key = url.toString() + ".properties"; - result = buildFile(key); + result = buildFile(url.toString()); PathPropertyUtils.save(result, properties); } diff --git a/src/fr/devinsy/statoolinfos/crawl/Crawler.java b/src/fr/devinsy/statoolinfos/crawl/Crawler.java index 3be9680..6790e67 100644 --- a/src/fr/devinsy/statoolinfos/crawl/Crawler.java +++ b/src/fr/devinsy/statoolinfos/crawl/Crawler.java @@ -21,8 +21,11 @@ package fr.devinsy.statoolinfos.crawl; import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +33,7 @@ import org.slf4j.LoggerFactory; import fr.devinsy.statoolinfos.core.Configuration; import fr.devinsy.statoolinfos.core.Factory; import fr.devinsy.statoolinfos.core.StatoolInfosException; +import fr.devinsy.statoolinfos.core.StatoolInfosUtils; import fr.devinsy.statoolinfos.properties.PathProperties; import fr.devinsy.statoolinfos.properties.PathProperty; import fr.devinsy.statoolinfos.properties.PathPropertyList; @@ -89,10 +93,18 @@ public class Crawler CrawlCache cache = configuration.getCrawlCache(); - PathProperties input = PathPropertyUtils.load(configuration.getCrawlInput()); + PathProperties input = PathPropertyUtils.load(configuration.getCrawlInputFile()); - cache.storeQuietly(input.getURL("federation.logo")); - cache.storeQuietly(input.getURL("organization.logo")); + if (configuration.isFederation()) + { + cache.store(input.get("federation.name"), configuration.getCrawlInputFile()); + cache.storeQuietly(input.getURL("federation.logo")); + } + else + { + cache.store(input.get("organization.name"), configuration.getCrawlInputFile()); + cache.storeQuietly(input.getURL("organization.logo")); + } PathProperties subs = input.getByPrefix("subs"); for (PathProperty property : subs) @@ -137,20 +149,29 @@ public class Crawler { logger.info("Crawling " + url); + // Crawl. File file = cache.store(url); - PathProperties properties = PathPropertyUtils.load(file); + // Build crawl data. PathProperties crawlSection = new PathPropertyList(); crawlSection.put("crawl.crawler", "StatoolInfos"); crawlSection.put("crawl.datetime", LocalDateTime.now().toString()); crawlSection.put("crawl.url", url.toString()); - properties.add(crawlSection); - cache.storeProperties(url, properties); + crawlSection.put("crawl.file.size", FileUtils.sizeOf(file)); + crawlSection.put("crawl.file.datetime", StatoolInfosUtils.urlLastModified(url).toString()); + crawlSection.put("crawl.file.sha1", DigestUtils.sha1Hex(FileUtils.readFileToByteArray(file))); + + // Add crawl data in crawled file. + String lines = crawlSection.toStringListFormatted().toStringSeparatedBy('\n'); + FileUtils.write(file, FileUtils.readFileToString(file, StandardCharsets.UTF_8) + "\n" + lines, StandardCharsets.UTF_8); + + // Crawl another resources. + PathProperties properties = PathPropertyUtils.load(file); cache.storeQuietly(properties.getURL("organization.logo")); cache.storeQuietly(properties.getURL("service.logo")); - // + // Crawl subs. PathProperties subs = properties.getByPrefix("subs"); for (PathProperty property : subs) { diff --git a/src/fr/devinsy/statoolinfos/htmlize/Htmlizer.java b/src/fr/devinsy/statoolinfos/htmlize/Htmlizer.java index ee8d0bd..3f03f76 100644 --- a/src/fr/devinsy/statoolinfos/htmlize/Htmlizer.java +++ b/src/fr/devinsy/statoolinfos/htmlize/Htmlizer.java @@ -35,8 +35,9 @@ import fr.devinsy.statoolinfos.core.Service; import fr.devinsy.statoolinfos.core.StatoolInfosException; import fr.devinsy.statoolinfos.core.StatoolInfosUtils; import fr.devinsy.statoolinfos.crawl.CrawlCache; -import fr.devinsy.statoolinfos.stats.PropertyStats; import fr.devinsy.statoolinfos.stats.StatAgent; +import fr.devinsy.statoolinfos.stats.properties.PropertyStats; +import fr.devinsy.statoolinfos.stats.propertyfiles.PropertiesFileStats; /** * The Class Htmlizer. @@ -212,8 +213,8 @@ public class Htmlizer // Manage the logo file. logger.info("Htmlize federation logo."); cache.restoreLogoTo(federation.getLogoURL(), new File(htmlizeDirectory, federation.getTechnicalName() + "-logo.png"), federation.getTechnicalName()); - logger.info("Htmlize federation properties file."); - FileUtils.copyFile(federation.getLocalFile(), new File(htmlizeDirectory, federation.getTechnicalName() + ".properties")); + logger.info("Htmlize federation properties files."); + FileUtils.copyFile(federation.getInputFile(), new File(htmlizeDirectory, federation.getTechnicalName() + ".properties")); // logger.info("Htmlize about page."); @@ -231,7 +232,7 @@ public class Htmlizer logger.info("Htmlize organization logo: {}.", organization.getName()); cache.restoreLogoTo(organization.getLogoURL(), new File(htmlizeDirectory, organization.getTechnicalName() + "-logo.png"), organization.getTechnicalName()); logger.info("Htmlize organization properties file: {}.", organization.getName()); - FileUtils.copyFile(organization.getLocalFile(), new File(htmlizeDirectory, organization.getTechnicalName() + ".properties")); + FileUtils.copyFile(organization.getInputFile(), new File(htmlizeDirectory, organization.getTechnicalName() + ".properties")); // logger.info("Htmlize organization page: {}.", organization.getName()); @@ -244,7 +245,7 @@ public class Htmlizer logger.info("Htmlize service logo: {}.", service.getName()); cache.restoreLogoTo(service.getLogoURL(), new File(htmlizeDirectory, organization.getTechnicalName() + "-" + service.getTechnicalName() + "-logo.png"), service.getTechnicalName()); logger.info("Htmlize service properties file: {}.", service.getName()); - FileUtils.copyFile(service.getLocalFile(), + FileUtils.copyFile(service.getInputFile(), new File(htmlizeDirectory, organization.getTechnicalName() + "-" + service.getTechnicalName() + ".properties")); logger.info("Htmlize service page: {}.", service.getName()); @@ -258,10 +259,12 @@ public class Htmlizer page = ServicesPage.build(federation.getAllServices()); FileUtils.write(new File(htmlizeDirectory, "services.xhtml"), page, StandardCharsets.UTF_8); - logger.info("Htmlize propertiesFiles page."); - page = PropertiesFilesPage.build(federation); - FileUtils.write(new File(htmlizeDirectory, "propertiesFiles.xhtml"), page, StandardCharsets.UTF_8); - + { + logger.info("Htmlize propertiesFiles page."); + PropertiesFileStats stats = StatAgent.statAllPropertiesFiles(federation, cache).sortByName(); + page = PropertiesFilesPage.build(stats); + FileUtils.write(new File(htmlizeDirectory, "propertiesFiles.xhtml"), page, StandardCharsets.UTF_8); + } // { logger.info("Htmlize propertyStats page."); diff --git a/src/fr/devinsy/statoolinfos/htmlize/PropertiesFilesPage.java b/src/fr/devinsy/statoolinfos/htmlize/PropertiesFilesPage.java index 3751ed5..847f030 100644 --- a/src/fr/devinsy/statoolinfos/htmlize/PropertiesFilesPage.java +++ b/src/fr/devinsy/statoolinfos/htmlize/PropertiesFilesPage.java @@ -25,9 +25,9 @@ import java.util.Locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import fr.devinsy.statoolinfos.core.Federation; -import fr.devinsy.statoolinfos.core.Organization; import fr.devinsy.statoolinfos.core.StatoolInfosException; +import fr.devinsy.statoolinfos.stats.propertyfiles.PropertiesFileStat; +import fr.devinsy.statoolinfos.stats.propertyfiles.PropertiesFileStats; import fr.devinsy.statoolinfos.util.BuildInformation; import fr.devinsy.xidyn.XidynException; import fr.devinsy.xidyn.data.TagDataManager; @@ -49,41 +49,37 @@ public class PropertiesFilesPage * @throws StatoolInfosException * the statool infos exception */ - public static String build(final Federation federation) throws StatoolInfosException + public static String build(final PropertiesFileStats stats) throws StatoolInfosException { String result; try { - logger.debug("Building federation page {}…", federation.getName()); + logger.debug("Building propertyFiles page."); TagDataManager data = new TagDataManager(); data.setContent("versionsup", BuildInformation.instance().version()); data.setContent("lastUpdateDate", LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH':'mm", Locale.FRANCE))); - data.setAttribute("federationRawButton", "href", federation.getTechnicalName() + ".properties"); - - data.setAttribute("federationLogo", "src", federation.getTechnicalName() + "-logo.png"); - data.setEscapedContent("federationName", federation.getName()); - data.setEscapedContent("federationDescription", federation.getDescription()); - data.setContent("organizationCount", federation.getOrganizations().size()); - data.setContent("serviceCount", federation.getServiceCount()); + data.setContent("fileCount", stats.size()); // int index = 0; - - // - - // - for (Organization organization : federation.getOrganizations()) + for (PropertiesFileStat stat : stats) { - data.setAttribute("organizationListLine", index, "organizationListLineNameLink", "href", organization.getTechnicalName() + ".xhtml"); - data.setAttribute("organizationListLine", index, "organizationListLineLogo", "src", organization.getTechnicalName() + "-logo.png"); - data.setEscapedContent("organizationListLine", index, "organizationListLineNameValue", organization.getName()); - data.setEscapedContent("organizationListLine", index, "organizationListLineUrlLink", organization.getWebsite()); - data.setAttribute("organizationListLine", index, "organizationListLineUrlLink", "href", organization.getWebsite()); - data.setContent("organizationListLine", index, "organizationListLineServiceCount", organization.getServiceCount()); + data.setAttribute("fileListLine", index, "fileListLineNameLink", "href", stat.getLocalName() + ".xhtml"); + data.setEscapedContent("fileListLine", index, "fileListLineNameLink", stat.getLocalName()); + + data.setAttribute("fileListLine", index, "fileListLineOwnerLink", "href", stat.getOrganization().getTechnicalName() + ".xhtml"); + data.setEscapedContent("fileListLine", index, "fileListLineNameValue", stat.getOrganization().getTechnicalName()); + data.setAttribute("fileListLine", index, "fileListLineOwnerLogo", "src", stat.getOrganization().getTechnicalName() + "-logo.png"); + + data.setContent("fileListLine", index, "fileListLineLineCount", stat.getLineCount()); + data.setContent("fileListLine", index, "fileListLineActiveCount", stat.getActiveLineCount()); + data.setContent("fileListLine", index, "fileListLineBlankPropertyCount", stat.getBlankPropertyCount()); + data.setContent("fileListLine", index, "fileListLineFilledPropertyCount", stat.getFilledPropertyCount()); + data.setContent("fileListLine", index, "fileListLineErrorCount", stat.getErrorCount()); index += 1; } diff --git a/src/fr/devinsy/statoolinfos/htmlize/PropertyStatsPage.java b/src/fr/devinsy/statoolinfos/htmlize/PropertyStatsPage.java index 22b9f29..9811ed0 100644 --- a/src/fr/devinsy/statoolinfos/htmlize/PropertyStatsPage.java +++ b/src/fr/devinsy/statoolinfos/htmlize/PropertyStatsPage.java @@ -27,8 +27,8 @@ import org.slf4j.LoggerFactory; import fr.devinsy.statoolinfos.core.StatoolInfosException; import fr.devinsy.statoolinfos.core.StatoolInfosUtils; -import fr.devinsy.statoolinfos.stats.PropertyStat; -import fr.devinsy.statoolinfos.stats.PropertyStats; +import fr.devinsy.statoolinfos.stats.properties.PropertyStat; +import fr.devinsy.statoolinfos.stats.properties.PropertyStats; import fr.devinsy.statoolinfos.util.BuildInformation; import fr.devinsy.xidyn.XidynException; import fr.devinsy.xidyn.data.TagDataManager; diff --git a/src/fr/devinsy/statoolinfos/htmlize/propertiesFiles.xhtml b/src/fr/devinsy/statoolinfos/htmlize/propertiesFiles.xhtml index 6baca5c..c5666ad 100644 --- a/src/fr/devinsy/statoolinfos/htmlize/propertiesFiles.xhtml +++ b/src/fr/devinsy/statoolinfos/htmlize/propertiesFiles.xhtml @@ -15,25 +15,34 @@
Nom | -Organisation | -Nombre de property | -Source | +Nom | +Organisation | +Lignes | +Propriétés | +Remplies | +Vides | +Erreurs |
---|---|---|---|---|---|---|---|---|---|---|
- - + n/a + | ++ + n/a | n/a | -n/a | +n/a | +n/a | +n/a | +n/a |