├── IPTC EXTRA User Manual.pdf ├── LICENCE ├── README.md ├── api ├── Dockerfile ├── README.md ├── extra-api.raml └── extra-api │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── org │ │ └── iptc │ │ └── extra │ │ └── api │ │ ├── Application.java │ │ ├── ApplicationProperties.java │ │ ├── CORSFilter.java │ │ ├── ServerApp.java │ │ ├── binder │ │ └── ApplicationBinder.java │ │ ├── databind │ │ ├── DocumentDeserializer.java │ │ ├── DocumentPagedResponseSerializer.java │ │ └── DocumentSerializer.java │ │ ├── datatypes │ │ ├── ClassificationInput.java │ │ ├── DocumentPagedResponse.java │ │ ├── Message.java │ │ └── PagedResponse.java │ │ └── resources │ │ ├── ClassificationsResource.java │ │ ├── CorporaResource.java │ │ ├── DictionariesResource.java │ │ ├── DocumentsResource.java │ │ ├── RulesResource.java │ │ ├── SchemasResource.java │ │ ├── TaxonomyResource.java │ │ └── ValidationsResource.java │ └── resources │ └── application.properties ├── docker-compose.yaml ├── documents-api ├── Dockerfile ├── api.py ├── index_documents.py ├── requirements.txt ├── sections.py └── topics.py ├── extra_platform_arch.png └── ui ├── Dockerfile ├── apache-config.conf └── html ├── .htaccess ├── .htpasswd ├── documents ├── css │ ├── jquery.comiseo.daterangepicker.css │ └── main.css ├── imgs │ ├── corpus-16.png │ ├── date-16.png │ ├── delete.png │ ├── link-16.png │ ├── loading.gif │ ├── search-12.png │ ├── sections-16.png │ ├── split-16.png │ ├── topics-16.png │ └── undo.png ├── index.html └── js │ ├── ajax_abort.js │ ├── daterangepicker.js │ ├── jquery.comiseo.daterangepicker.js │ ├── jquery.easy-autocomplete.js │ ├── jquery.reveal.js │ ├── jquery.twbsPagination.min.js │ ├── main.js │ ├── moment.js │ └── xml.js ├── editor ├── css │ └── main.css ├── imgs │ ├── add-icon.png │ ├── back.png │ ├── delete.png │ └── search-12.png ├── index.html └── js │ ├── jquery.easy-autocomplete.js │ ├── jquery.reveal.js │ ├── jquery.twbsPagination.min.js │ ├── jquery.wookmark.js │ └── main.js ├── schemas ├── css │ └── main.css ├── imgs │ └── delete.png ├── index.html └── js │ ├── jquery.reveal.js │ └── main.js ├── tagging ├── css │ └── main.css ├── imgs │ ├── add-icon.png │ ├── delete.png │ └── search-12.png ├── index.html └── js │ ├── jquery.twbsPagination.min.js │ ├── jquery.wookmark.js │ └── main.js └── taxonomies ├── css └── main.css ├── imgs ├── delete.png ├── edit.png ├── link.png └── search-12.png ├── index.html └── js ├── jquery.reveal.js ├── jquery.twbsPagination.min.js └── main.js /IPTC EXTRA User Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/IPTC EXTRA User Manual.pdf -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017-2019 International Press Telecommunications Council 4 | https://www.iptc.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extra-ext 2 | API implementation, User Interface, and more modules of the IPTC EXTRA project. For a more detailed description of the integrated toolkit and its functionalities, please read the [user manual](https://github.com/iptc/extra-ext/blob/master/IPTC%20EXTRA%20User%20Manual.pdf). 3 | 4 | ## Description 5 | EXTRA platform is built upon a set of services, deployed and running inside [Docker](https://www.docker.com/) containers. More specifically, the services of the platform are the following ones: 6 | 7 | * [mongodb](https://www.mongodb.com/) - MongoDB is an open-source, document database that is used to store rules, schemas, dictionaries, taxonomies and topics. Access to MongoDB is not allowed directly outside Docker. To access the stored objects, the API methods must be used. 8 | * [elasticsearch](https://www.elastic.co/products/elasticsearch) - Elastic search used for the indexing of documents and rules. Indexed document are used for testing of rules during their development. Rules are indexed into Percolate index of Elastic Search to be used for document classification. 9 | * [documents-api](https://github.com/iptc/extra-ext/tree/master/documents-api) - An API for documents indexed in ElasticSearch, built using [Python Flask](http://flask.pocoo.org/) library. 10 | * [api](https://github.com/iptc/extra-ext/tree/master/api) - EXTRA API built using [Jersey framework](https://jersey.github.io/). That API exposes the main functionality of EXTRA, including management of rules, schemas, taxonomies and topics (create, update, delete), and also document retrieval and tagging. A description of the API in RAML can be found in [this link](api/extra-api.raml). 11 | * [ui](https://github.com/iptc/extra-ext/tree/master/ui) - A, HTML web interface on top of Apache web server. Uses the previous two APIs to expose the main functionality of EXTRA platform in a user friendly way. Consists of pages for rule writing and testing, management of taxonomies and schemas, document searching and document tagging. 12 | 13 | The architecture of EXTRA platform is depicted in the following figure. To make deployment easier, the platform is described in a [docker-compose file](https://github.com/iptc/extra-ext/blob/master/docker-compose.yaml). 14 | 15 | ![EXTRA platform architecture](extra_platform_arch.png) 16 | 17 | 18 | ## Configuration 19 | 20 | To configure the platform for development, [docker-compose file](https://github.com/iptc/extra-ext/blob/master/docker-compose.yaml) must be edited. 21 | 22 | There are three points need to be revised to be able to deploy EXTRA platform. 23 | 24 | ### MongoDB 25 | 26 | 27 | First off all, you have to specify data volume of mongodb to ensure data persistence. The directory /data/db inside the container that runs mongodb has to be mounted in a directory on the host machine's local filesystem. To define the local directory change `` in the below section. 28 | 29 | ```yaml 30 | mongodb: 31 | image: mongo:3.2.11 32 | volumes: 33 | - :/data/db 34 | ``` 35 | 36 | ### Elastic Search 37 | 38 | In the same way the directory to which elastic search keeps its indexes has to be specified: 39 | 40 | ```yaml 41 | elasticsearch: 42 | image: docker.elastic.co/elasticsearch/elasticsearch:5.2.0 43 | ports: 44 | - 9200:9200 45 | - 9300:9300 46 | volumes: 47 | - :/usr/share/elasticsearch/data 48 | ``` 49 | 50 | Make sure that user that executes the docker images has r/w rights to the elastic search data directory. The easiest way is to give full access: 51 | 52 | ```sh 53 | $chmod 777 -R 54 | ``` 55 | **important** The *vm_max_map_count* kernel setting needs to be set to at least *262144* for production use. 56 | To apply the setting on a live system type: `sysctl -w vm.max_map_count=262144` 57 | 58 | By default, the services allocate and expose the following default ports: 59 | * **elastic search:** *9200*, *9300* -> *9200*, *9300* 60 | * **documents-api:** *5000* -> *5000* 61 | * **api:** *8888* -> *8888* 62 | * **ui:** *80* -> *80* 63 | 64 | To change the default ports, edit the corresponding section in docker-compose.yaml file. For example to change the 65 | port *8888* used by EXTRA API to another port, e.g. *9999*, edit the file as follows: 66 | 67 | ```yaml 68 | api: 69 | ports: 70 | - 9999:8888 71 | ``` 72 | 73 | 74 | ## Deployment 75 | 76 | To start EXTRA platform: 77 | 78 | ```sh 79 | $ cd extra 80 | $ docker-compose up -d 81 | ``` 82 | 83 | This will create or download images and pull in the necessary dependencies for each service. Once done, it runs the Docker and map the ports to whatever is specified in the [docker-compose.yml](https://github.com/iptc/extra-ext/blob/master/docker-compose.yaml) file. 84 | 85 | Verify the deployment by typing: 86 | 87 | ```sh 88 | $ docker ps 89 | ``` 90 | There should be 5 running containers. 91 | 92 | To stop the running platform: 93 | 94 | ```sh 95 | $ cd extra 96 | $ docker-compose down 97 | ``` 98 | 99 | ## Contact for further details about the project 100 | 101 | Technical details: Manos Schinas (manosetro@iti.gr), Symeon Papadopoulos (papadop@iti.gr) 102 | Project as a whole: [IPTC](https://iptc.org) (office@iptc.org) 103 | 104 | ## See the other repositories of the IPTC EXTRA project: 105 | 106 | * [extra-core](https://github.com/iptc/extra-core) - Core implementation of EXTRA 107 | * [extra-examples](https://github.com/iptc/extra-examples) - Examples developed for EXTRA project 108 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8 2 | MAINTAINER Manos Schinas manosetro@iti.gr 3 | 4 | RUN apt-get update && \ 5 | DEBIAN_FRONTEND=noninteractive apt-get install -y maven 6 | 7 | RUN mkdir -p /opt/app 8 | WORKDIR /opt/app 9 | 10 | 11 | # selectively add the POM file and install dependencies 12 | COPY extra-api/pom.xml /opt/app/ 13 | RUN mvn install 14 | 15 | # add rest of the project and build it 16 | COPY extra-api/src /opt/app/src 17 | RUN mvn package 18 | 19 | # local application port 20 | EXPOSE 8888 21 | 22 | # execute it 23 | CMD ["mvn", "exec:java"] 24 | -------------------------------------------------------------------------------- /api/extra-api/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | 6 | org.iptc 7 | extra-api 8 | jar 9 | 0.1.1-SNAPSHOT 10 | extra-api 11 | 12 | 13 | 14 | 15 | org.glassfish.jersey 16 | jersey-bom 17 | ${jersey.version} 18 | pom 19 | import 20 | 21 | 22 | 23 | 24 | 25 | org.sonatype.oss 26 | oss-parent 27 | 7 28 | 29 | 30 | 31 | 32 | org.glassfish.jersey.containers 33 | jersey-container-grizzly2-http 34 | 35 | 41 | 42 | org.glassfish.jersey.media 43 | jersey-media-json-jackson 44 | 45 | 46 | junit 47 | junit 48 | 4.13.2 49 | test 50 | 51 | 52 | org.iptc 53 | extra-core 54 | 0.1.1-SNAPSHOT 55 | 56 | 57 | 58 | 59 | 60 | 61 | maven-assembly-plugin 62 | 63 | 64 | 65 | org.iptc.extra.api.ServerApp 66 | 67 | 68 | 69 | jar-with-dependencies 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-compiler-plugin 76 | 2.5.1 77 | true 78 | 79 | 1.8 80 | 1.8 81 | 82 | 83 | 84 | org.codehaus.mojo 85 | exec-maven-plugin 86 | 1.2.1 87 | 88 | 89 | 90 | java 91 | 92 | 93 | 94 | 95 | org.iptc.extra.api.ServerApp 96 | 97 | 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-shade-plugin 102 | 1.7.1 103 | 104 | 105 | StreamsManager 106 | package 107 | 108 | shade 109 | 110 | 111 | true 112 | jar-with-dependencies 113 | 114 | 115 | *:* 116 | 117 | META-INF/*.SF 118 | META-INF/*.DSA 119 | META-INF/*.RSA 120 | 121 | 122 | 123 | 124 | 125 | 126 | org.iptc.extra.api.ServerApp 127 | 123 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | release 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-assembly-plugin 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 2.25.1 155 | UTF-8 156 | 157 | 158 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/Application.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api; 2 | 3 | import java.util.Properties; 4 | 5 | import org.glassfish.jersey.server.ResourceConfig; 6 | import org.iptc.extra.api.binder.ApplicationBinder; 7 | import org.iptc.extra.api.databind.DocumentDeserializer; 8 | import org.iptc.extra.api.databind.DocumentPagedResponseSerializer; 9 | import org.iptc.extra.api.databind.DocumentSerializer; 10 | import org.iptc.extra.api.datatypes.DocumentPagedResponse; 11 | import org.iptc.extra.core.types.document.Document; 12 | 13 | import com.fasterxml.jackson.annotation.JsonInclude; 14 | import com.fasterxml.jackson.databind.ObjectMapper; 15 | import com.fasterxml.jackson.databind.module.SimpleModule; 16 | import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; 17 | 18 | /** 19 | * @author Petr Bouda (petr.bouda at oracle.com) 20 | */ 21 | public class Application extends ResourceConfig { 22 | 23 | 24 | 25 | public Application(Properties properties) { 26 | packages(Application.class.getPackage().getName()); 27 | register(new ApplicationBinder(properties)); 28 | register(createJacksonJaxbJsonProvider()); 29 | 30 | //register(createMoxyJsonResolver()); 31 | } 32 | 33 | public static JacksonJaxbJsonProvider createJacksonJaxbJsonProvider() { 34 | ObjectMapper mapper = new ObjectMapper(); 35 | 36 | mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); 37 | mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 38 | 39 | SimpleModule module = new SimpleModule(); 40 | module.addSerializer(Document.class, new DocumentSerializer()); 41 | module.addDeserializer(Document.class, new DocumentDeserializer()); 42 | module.addSerializer(DocumentPagedResponse.class, new DocumentPagedResponseSerializer()); 43 | 44 | mapper.registerModule(module); 45 | 46 | JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(); 47 | provider.setMapper(mapper); 48 | 49 | return provider; 50 | } 51 | 52 | /* 53 | public static ContextResolver createMoxyJsonResolver() { 54 | final MoxyJsonConfig moxyJsonConfig = new MoxyJsonConfig(); 55 | Map namespacePrefixMapper = new HashMap(1); 56 | namespacePrefixMapper.put("http://www.w3.org/2001/XMLSchema-instance", "xsi"); 57 | moxyJsonConfig.setNamespacePrefixMapper(namespacePrefixMapper).setNamespaceSeparator(':'); 58 | return moxyJsonConfig.resolver(); 59 | } 60 | */ 61 | 62 | } -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/ApplicationProperties.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api; 2 | 3 | public final class ApplicationProperties { 4 | 5 | public static final String MONGODB_HOST = "mongodb.host"; 6 | 7 | public static final String MONGODB_PORT = "mongodb.port"; 8 | 9 | public static final String MONGODB_DATABASE = "mongodb.database"; 10 | 11 | public static final String MONGODB_USERNAME = "mongodb.username"; 12 | 13 | public static final String MONGODB_PASSWORD = "mongodb.password"; 14 | 15 | public static final String ES_HOST = "es.host"; 16 | 17 | public static final String ES_PORT = "es.port"; 18 | 19 | public static final String BASE_URI = "base.uri"; 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/CORSFilter.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api; 2 | import java.io.IOException; 3 | import javax.ws.rs.container.ContainerRequestContext; 4 | import javax.ws.rs.container.ContainerResponseContext; 5 | import javax.ws.rs.container.ContainerResponseFilter; 6 | import javax.ws.rs.ext.Provider; 7 | 8 | @Provider 9 | public class CORSFilter implements ContainerResponseFilter { 10 | 11 | @Override 12 | public void filter(ContainerRequestContext request, 13 | ContainerResponseContext response) throws IOException { 14 | response.getHeaders().add("Access-Control-Allow-Origin", "*"); 15 | response.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); 16 | response.getHeaders().add("Access-Control-Allow-Credentials", "true"); 17 | response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/ServerApp.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api; 2 | 3 | import org.glassfish.grizzly.http.server.HttpServer; 4 | import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; 5 | import org.iptc.extra.api.binder.ApplicationBinder; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.net.URI; 10 | import java.util.Properties; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | /** 15 | * App main class. 16 | * 17 | */ 18 | public class ServerApp { 19 | 20 | public static final String PROPS_FILE = "application.properties"; 21 | 22 | public static void main(String[] args) { 23 | try { 24 | Properties properties = getProperties(PROPS_FILE); 25 | 26 | String baseUri = properties.getProperty(ApplicationProperties.BASE_URI, "http://0.0.0.0:8888/extra/api"); 27 | 28 | URI uri = URI.create(baseUri); 29 | HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri, new Application(properties), false); 30 | 31 | Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow)); 32 | server.start(); 33 | 34 | System.out.println("Application started.\n" 35 | + "REST API: " + uri + "\n" 36 | + "Stop the application using CTRL+C"); 37 | 38 | Thread.currentThread().join(); 39 | } catch (IOException | InterruptedException ex) { 40 | Logger.getLogger(ServerApp.class.getName()).log(Level.SEVERE, null, ex); 41 | } 42 | } 43 | 44 | private static Properties getProperties(String path) { 45 | try (InputStream inputStream = ApplicationBinder.class.getClassLoader().getResourceAsStream(path)) { 46 | Properties properties = new Properties(); 47 | properties.load(inputStream); 48 | return properties; 49 | } catch (Exception e) { 50 | 51 | } 52 | return null; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/binder/ApplicationBinder.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.binder; 2 | 3 | import java.net.UnknownHostException; 4 | import java.util.Properties; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import org.glassfish.hk2.utilities.binding.AbstractBinder; 9 | import org.iptc.extra.api.ApplicationProperties; 10 | import org.iptc.extra.core.daos.CorporaDAO; 11 | import org.iptc.extra.core.daos.DictionariesDAO; 12 | import org.iptc.extra.core.daos.GroupDAO; 13 | import org.iptc.extra.core.daos.RulesDAO; 14 | import org.iptc.extra.core.daos.SchemasDAO; 15 | import org.iptc.extra.core.daos.TaxonomiesDAO; 16 | import org.iptc.extra.core.daos.TopicsDAO; 17 | import org.iptc.extra.core.es.ElasticSearchClient; 18 | import org.mongodb.morphia.Datastore; 19 | import org.mongodb.morphia.Morphia; 20 | 21 | import com.mongodb.MongoClient; 22 | 23 | /** 24 | * @author Manos Schinas (manosetro@iti.gr) 25 | */ 26 | public class ApplicationBinder extends AbstractBinder { 27 | 28 | private final Properties properties; 29 | 30 | public ApplicationBinder(Properties properties) { 31 | this.properties = properties; 32 | } 33 | 34 | @Override 35 | protected void configure() { 36 | 37 | Morphia morphia = new Morphia(); 38 | bind(morphia).to(Morphia.class); 39 | 40 | if (properties != null) { 41 | for (String name : properties.stringPropertyNames()) { 42 | String value = properties.getProperty(name); 43 | bind(value).to(String.class).named(name); 44 | } 45 | 46 | String hostname = properties.getProperty(ApplicationProperties.MONGODB_HOST, "127.0.0.1"); 47 | String port = properties.getProperty(ApplicationProperties.MONGODB_PORT, "27017"); 48 | String database = properties.getProperty(ApplicationProperties.MONGODB_DATABASE, "test"); 49 | 50 | MongoClient mongoClient = new MongoClient(hostname, Integer.parseInt(port)); 51 | final Datastore datastore = morphia.createDatastore(mongoClient, database); 52 | datastore.ensureIndexes(); 53 | 54 | bind(Datastore.class).to(Singleton.class); 55 | bind(datastore).to(Datastore.class); 56 | 57 | RulesDAO rulesDAO = new RulesDAO(datastore); 58 | bind(rulesDAO).to(RulesDAO.class); 59 | 60 | GroupDAO groupDAO = new GroupDAO(datastore); 61 | bind(groupDAO).to(GroupDAO.class); 62 | 63 | SchemasDAO schemasDAO = new SchemasDAO(datastore); 64 | bind(schemasDAO).to(SchemasDAO.class); 65 | 66 | DictionariesDAO dictionariesDAO = new DictionariesDAO(datastore); 67 | bind(dictionariesDAO).to(DictionariesDAO.class); 68 | 69 | TopicsDAO topicsDAO = new TopicsDAO(datastore); 70 | bind(topicsDAO).to(TopicsDAO.class); 71 | 72 | TaxonomiesDAO taxonomiesDAO = new TaxonomiesDAO(datastore); 73 | bind(taxonomiesDAO).to(TaxonomiesDAO.class); 74 | 75 | CorporaDAO corporaDAO = new CorporaDAO(datastore); 76 | bind(corporaDAO).to(CorporaDAO.class); 77 | 78 | String esHostname = properties.getProperty(ApplicationProperties.ES_HOST, "127.0.0.1"); 79 | int esPort = Integer.parseInt(properties.getProperty(ApplicationProperties.ES_PORT, "9300")); 80 | ElasticSearchClient es; 81 | try { 82 | es = new ElasticSearchClient(esHostname, esPort); 83 | bind(es).to(ElasticSearchClient.class); 84 | } catch (UnknownHostException e) { 85 | e.printStackTrace(); 86 | } 87 | } 88 | 89 | } 90 | } -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/databind/DocumentDeserializer.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.databind; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | 8 | import org.iptc.extra.core.types.document.Document; 9 | import org.iptc.extra.core.types.document.DocumentTopic; 10 | 11 | import com.fasterxml.jackson.core.JsonParser; 12 | import com.fasterxml.jackson.core.JsonProcessingException; 13 | import com.fasterxml.jackson.databind.DeserializationContext; 14 | import com.fasterxml.jackson.databind.JsonDeserializer; 15 | import com.fasterxml.jackson.databind.JsonNode; 16 | import com.fasterxml.jackson.databind.node.JsonNodeType; 17 | 18 | public class DocumentDeserializer extends JsonDeserializer { 19 | 20 | @Override 21 | public Document deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { 22 | 23 | Document document = new Document(); 24 | 25 | JsonNode node = jp.getCodec().readTree(jp); 26 | Iterator it = node.fieldNames(); 27 | while(it.hasNext()) { 28 | String fieldName = it.next(); 29 | JsonNode fieldNode = node.get(fieldName); 30 | 31 | if(fieldNode.getNodeType() == JsonNodeType.ARRAY && fieldName.equals("topics")) { 32 | 33 | List topics = new ArrayList(); 34 | 35 | Iterator topicsIterator = fieldNode.elements(); 36 | while(topicsIterator.hasNext()) { 37 | JsonNode topicJsonNode = topicsIterator.next(); 38 | 39 | DocumentTopic topic = new DocumentTopic(); 40 | if(topicJsonNode.has("topicId")) { 41 | topic.setTopicId(topicJsonNode.get("topicId").asText()); 42 | 43 | if(topicJsonNode.has("url")) { 44 | topic.setUrl(topicJsonNode.get("url").asText()); 45 | } 46 | if(topicJsonNode.has("name")) { 47 | topic.setName(topicJsonNode.get("name").asText()); 48 | } 49 | if(topicJsonNode.has("association")) { 50 | topic.setAssociation(topicJsonNode.get("association").asText()); 51 | } 52 | if(topicJsonNode.has("parentTopic")) { 53 | topic.setParentTopic(topicJsonNode.get("parentTopic").asText()); 54 | } 55 | 56 | topics.add(topic); 57 | } 58 | document.setTopics(topics); 59 | } 60 | } 61 | else { 62 | String value = fieldNode.asText(); 63 | if(value != null) { 64 | if(fieldName.equals("id")) { 65 | document.setId(value); 66 | } 67 | else { 68 | document.addField(fieldName, value); 69 | } 70 | } 71 | } 72 | } 73 | 74 | return document; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/databind/DocumentPagedResponseSerializer.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.databind; 2 | 3 | import java.io.IOException; 4 | 5 | import org.iptc.extra.api.datatypes.DocumentPagedResponse; 6 | 7 | import com.fasterxml.jackson.core.JsonGenerator; 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.JsonSerializer; 10 | import com.fasterxml.jackson.databind.SerializerProvider; 11 | 12 | public class DocumentPagedResponseSerializer extends JsonSerializer { 13 | 14 | @Override 15 | public void serialize(DocumentPagedResponse resp, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { 16 | gen.writeStartObject(); 17 | 18 | gen.writeNumberField("page", resp.getPage()); 19 | gen.writeNumberField("total", resp.getTotal()); 20 | gen.writeNumberField("nPerPage", resp.getnPerPage()); 21 | 22 | gen.writeArrayFieldStart("entries"); 23 | for(Object entry : resp.getEntries()) { 24 | gen.writeObject(entry); 25 | } 26 | gen.writeEndArray(); 27 | 28 | gen.writeFieldName("annotations"); 29 | gen.writeStartObject(); 30 | for(String key : resp.getAnnotations().keySet()) { 31 | gen.writeStringField(key, resp.getAnnotation(key).toString()); 32 | } 33 | gen.writeEndObject(); 34 | 35 | gen.writeEndObject(); 36 | } 37 | } -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/databind/DocumentSerializer.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.databind; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | import org.iptc.extra.core.types.document.Document; 7 | import org.iptc.extra.core.types.document.DocumentField; 8 | import org.iptc.extra.core.types.document.Paragraph; 9 | import org.iptc.extra.core.types.document.StructuredTextField; 10 | import org.iptc.extra.core.types.document.TextField; 11 | 12 | import com.fasterxml.jackson.core.JsonGenerator; 13 | import com.fasterxml.jackson.core.JsonProcessingException; 14 | import com.fasterxml.jackson.databind.JsonSerializer; 15 | import com.fasterxml.jackson.databind.SerializerProvider; 16 | 17 | public class DocumentSerializer extends JsonSerializer { 18 | 19 | @Override 20 | public void serialize(Document document, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { 21 | gen.writeStartObject(); 22 | 23 | if(document.getId() != null) { 24 | gen.writeStringField("id", document.getId()); 25 | } 26 | 27 | for(String fieldName : document.getFieldNames()) { 28 | DocumentField field = document.get(fieldName); 29 | if(field instanceof StructuredTextField) { 30 | StructuredTextField structuredTextField = (StructuredTextField) field; 31 | gen.writeStringField(fieldName, structuredTextField.getValue()); 32 | 33 | List paragraphs = structuredTextField.getParagraphs(); 34 | gen.writeArrayFieldStart(fieldName + "_paragraphs"); 35 | for(Paragraph paragraph : paragraphs) { 36 | gen.writeStartObject(); 37 | gen.writeStringField("paragraph", paragraph.toString()); 38 | gen.writeEndObject(); 39 | } 40 | gen.writeEndArray(); 41 | } 42 | else if(field instanceof TextField) { 43 | gen.writeStringField(fieldName, ((TextField) field).getValue()); 44 | } 45 | 46 | } 47 | gen.writeEndObject(); 48 | } 49 | } -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/datatypes/ClassificationInput.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.datatypes; 2 | 3 | import org.iptc.extra.core.types.document.Document; 4 | 5 | public class ClassificationInput { 6 | 7 | private Document document; 8 | 9 | public Document getDocument() { 10 | return document; 11 | } 12 | 13 | public void setDocument(Document document) { 14 | this.document = document; 15 | } 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/datatypes/DocumentPagedResponse.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.datatypes; 2 | 3 | import org.iptc.extra.core.types.document.Document; 4 | 5 | public class DocumentPagedResponse extends PagedResponse{ 6 | 7 | } 8 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/datatypes/Message.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.datatypes; 2 | 3 | import javax.xml.bind.annotation.XmlRootElement; 4 | 5 | @XmlRootElement 6 | public class Message { 7 | 8 | public Message() { 9 | 10 | } 11 | 12 | public Message(String message) { 13 | this.message = message; 14 | } 15 | 16 | private String message; 17 | 18 | public String getMessage() { 19 | return message; 20 | } 21 | 22 | public void setMessage(String message) { 23 | this.message = message; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/datatypes/PagedResponse.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.datatypes; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import javax.xml.bind.annotation.XmlList; 9 | import javax.xml.bind.annotation.XmlRootElement; 10 | 11 | @XmlRootElement(name = "results") 12 | public class PagedResponse { 13 | 14 | private int page; 15 | private int nPerPage; 16 | private long total; 17 | 18 | @XmlList 19 | private List entries = new ArrayList(); 20 | 21 | @XmlList 22 | private Map annotations = new HashMap(); 23 | 24 | public PagedResponse() { 25 | 26 | } 27 | 28 | public int getPage() { 29 | return page; 30 | } 31 | 32 | public void setPage(int page) { 33 | this.page = page; 34 | } 35 | 36 | public int getnPerPage() { 37 | return nPerPage; 38 | } 39 | 40 | public void setnPerPage(int nPerPage) { 41 | this.nPerPage = nPerPage; 42 | } 43 | 44 | public long getTotal() { 45 | return total; 46 | } 47 | 48 | public void setTotal(long total) { 49 | this.total = total; 50 | } 51 | 52 | public List getEntries() { 53 | return entries; 54 | } 55 | 56 | public void setEntries(List entries) { 57 | this.entries = entries; 58 | } 59 | 60 | public void addAnnotation(String key, Object value) { 61 | annotations.put(key, value); 62 | } 63 | 64 | public Map getAnnotations() { 65 | return annotations; 66 | } 67 | 68 | public Object getAnnotation(String key) { 69 | return annotations.get(key); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/ClassificationsResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import javax.inject.Inject; 8 | import javax.inject.Singleton; 9 | import javax.ws.rs.Consumes; 10 | import javax.ws.rs.DefaultValue; 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.Path; 13 | import javax.ws.rs.Produces; 14 | import javax.ws.rs.QueryParam; 15 | import javax.ws.rs.core.GenericEntity; 16 | import javax.ws.rs.core.MediaType; 17 | import javax.ws.rs.core.Response; 18 | 19 | import org.iptc.extra.api.datatypes.ClassificationInput; 20 | import org.iptc.extra.api.datatypes.Message; 21 | import org.iptc.extra.api.datatypes.PagedResponse; 22 | import org.iptc.extra.core.daos.RulesDAO; 23 | import org.iptc.extra.core.daos.SchemasDAO; 24 | import org.iptc.extra.core.es.ElasticSearchClient; 25 | import org.iptc.extra.core.es.ElasticSearchResponse; 26 | import org.iptc.extra.core.types.Rule; 27 | import org.iptc.extra.core.types.Schema; 28 | import org.iptc.extra.core.types.Schema.Field; 29 | import org.iptc.extra.core.types.document.Document; 30 | import org.iptc.extra.core.types.document.DocumentField; 31 | import org.iptc.extra.core.types.document.Paragraph; 32 | import org.iptc.extra.core.types.document.StructuredTextField; 33 | import org.iptc.extra.core.utils.TextUtils; 34 | 35 | 36 | @Singleton 37 | @Path("classifications") 38 | public class ClassificationsResource { 39 | 40 | @Inject 41 | private ElasticSearchClient es; 42 | 43 | @Inject 44 | private SchemasDAO schemasDAO; 45 | 46 | @Inject 47 | private RulesDAO rulesDAO; 48 | 49 | @POST 50 | @Produces(MediaType.APPLICATION_JSON) 51 | @Consumes(MediaType.APPLICATION_JSON) 52 | public Response postDocument(ClassificationInput classificationInput, 53 | @QueryParam("schemaId") String schemaId, 54 | @QueryParam("groupId") String groupId, 55 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage, 56 | @DefaultValue("1") @QueryParam("page") int page) { 57 | 58 | try { 59 | Schema schema = schemasDAO.get(schemaId); 60 | if(schema == null) { 61 | Message msg = new Message("Schema " + schemaId + " does not exist"); 62 | return Response.status(404).entity(msg).build(); 63 | } 64 | 65 | Document document = classificationInput.getDocument(); 66 | 67 | Set documentFields = document.getFieldNames(); 68 | Set schemaFields = schema.getFieldNames(); 69 | if(!schemaFields.containsAll(documentFields)) { 70 | documentFields.removeAll(schemaFields); 71 | Message msg = new Message("Document contains unknown fields: " + documentFields); 72 | return Response.status(400).entity(msg).build(); 73 | } 74 | 75 | document = processDocument(document, schema); 76 | 77 | List rules = new ArrayList(); 78 | ElasticSearchResponse result = es.findRules(document, schema.getId(), groupId, page, nPerPage); 79 | for(String ruleId : result.getResults()) { 80 | Rule rule = rulesDAO.get(ruleId); 81 | if(rule != null) { 82 | rules.add(rule); 83 | } 84 | } 85 | 86 | PagedResponse response = new PagedResponse(); 87 | response.setEntries(rules); 88 | response.setPage(page); 89 | response.setnPerPage(nPerPage); 90 | response.setTotal(result.getFound()); 91 | 92 | GenericEntity > entity = new GenericEntity >(response) {}; 93 | return Response.ok(entity).build(); 94 | 95 | } catch (Exception e) { 96 | e.printStackTrace(); 97 | Message msg = new Message("Classification failed!"); 98 | return Response.status(400).entity(msg).build(); 99 | } 100 | 101 | } 102 | 103 | private Document processDocument(Document document, Schema schema) { 104 | 105 | Set fieldNames = document.getFieldNames(); 106 | for(String fieldName : fieldNames) { 107 | DocumentField documentFieldValue = document.get(fieldName); 108 | Field field = schema.getField(fieldName); 109 | if(field.hasParagraphs || field.hasSentences) { 110 | if(!(documentFieldValue instanceof StructuredTextField)) { 111 | String textValue = documentFieldValue.toString(); 112 | 113 | StructuredTextField structuredField = new StructuredTextField(); 114 | structuredField.setValue(textValue); 115 | 116 | List paragraphs = TextUtils.getParagraphs(textValue); 117 | 118 | if(field.hasParagraphs && !paragraphs.isEmpty()) { 119 | for(String paragraphText : paragraphs) { 120 | Paragraph paragraph = new Paragraph(paragraphText); 121 | structuredField.addParagraph(paragraph); 122 | } 123 | } 124 | else { 125 | Paragraph paragraph = new Paragraph(textValue); 126 | structuredField.addParagraph(paragraph); 127 | } 128 | document.addField(fieldName, structuredField); 129 | } 130 | } 131 | 132 | if (field.textual) { 133 | documentFieldValue = document.get(fieldName); 134 | document.addField("raw_" + fieldName, documentFieldValue); 135 | document.addField("stemmed_" + fieldName, documentFieldValue); 136 | document.addField("case_sensitive__" + fieldName, documentFieldValue); 137 | document.addField("literal__" + fieldName, documentFieldValue); 138 | } 139 | } 140 | 141 | return document; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/CorporaResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | import javax.ws.rs.Consumes; 8 | import javax.ws.rs.DELETE; 9 | import javax.ws.rs.DefaultValue; 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.PUT; 13 | import javax.ws.rs.Path; 14 | import javax.ws.rs.PathParam; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.QueryParam; 17 | import javax.ws.rs.core.MediaType; 18 | import javax.ws.rs.core.Response; 19 | 20 | import org.bson.types.ObjectId; 21 | import org.iptc.extra.api.datatypes.Message; 22 | import org.iptc.extra.api.datatypes.PagedResponse; 23 | import org.iptc.extra.core.daos.CorporaDAO; 24 | import org.iptc.extra.core.daos.SchemasDAO; 25 | import org.iptc.extra.core.daos.TaxonomiesDAO; 26 | import org.iptc.extra.core.es.ElasticSearchClient; 27 | import org.iptc.extra.core.es.ElasticSearchResponse; 28 | import org.iptc.extra.core.types.Corpus; 29 | import org.iptc.extra.core.types.Schema; 30 | import org.iptc.extra.core.types.Taxonomy; 31 | import org.iptc.extra.core.types.document.Document; 32 | import org.mongodb.morphia.Key; 33 | import org.mongodb.morphia.query.FindOptions; 34 | import org.mongodb.morphia.query.Query; 35 | import org.mongodb.morphia.query.QueryResults; 36 | import org.mongodb.morphia.query.UpdateOperations; 37 | 38 | /** 39 | * Coprora resource (exposed at "/corpora" path) 40 | */ 41 | @Singleton 42 | @Path("corpora") 43 | public class CorporaResource { 44 | 45 | @Inject 46 | private CorporaDAO dao; 47 | 48 | @Inject 49 | private SchemasDAO schemasDAO; 50 | 51 | @Inject 52 | private TaxonomiesDAO taxonomiesDAO; 53 | 54 | @Inject 55 | private ElasticSearchClient es; 56 | 57 | 58 | @GET 59 | @Produces(MediaType.APPLICATION_JSON) 60 | public Response getCoprora( 61 | @QueryParam("taxonomy") String taxonomy, 62 | @DefaultValue("1") @QueryParam("page") int page, 63 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage) { 64 | 65 | try { 66 | Query query = dao.createQuery(); 67 | 68 | if(taxonomy != null && !taxonomy.equals("")) { 69 | query = query.field("taxonomyId").equal(taxonomy); 70 | } 71 | 72 | QueryResults result = dao.find(query); 73 | 74 | FindOptions options = new FindOptions().skip((page-1)*nPerPage).limit(nPerPage); 75 | 76 | long total = result.count(); 77 | List corpora = result.asList(options); 78 | 79 | PagedResponse response = new PagedResponse(); 80 | response.setEntries(corpora); 81 | response.setPage(page); 82 | response.setnPerPage(nPerPage); 83 | response.setTotal(total); 84 | 85 | return Response.ok(response).build(); 86 | } 87 | catch(Exception e) { 88 | Message msg = new Message(e.getMessage()); 89 | return Response.status(400).entity(msg).build(); 90 | } 91 | } 92 | 93 | @POST 94 | @Produces(MediaType.APPLICATION_JSON) 95 | @Consumes(MediaType.APPLICATION_JSON) 96 | public Response postCorpus(Corpus corpus) { 97 | try { 98 | String id = corpus.getId(); 99 | if(id != null && dao.exists(id)) { 100 | Message msg = new Message("Conflict. Corpus " + id + " already exists."); 101 | return Response.status(409).entity(msg).build(); 102 | } 103 | else { 104 | // set creation date 105 | corpus.setCreatedAt(System.currentTimeMillis()); 106 | 107 | Key createdCorpusKey = dao.save(corpus); 108 | corpus.setId(createdCorpusKey.getId().toString()); 109 | 110 | Schema schema = schemasDAO.get(corpus.getSchemaId()); 111 | if(schema == null) { 112 | Message msg = new Message("Cannot create index for corpus " + corpus.getId()+ ". Schema " + corpus.getSchemaId() + " does not exist"); 113 | return Response.status(400).entity(msg).build(); 114 | } 115 | corpus.setSchema(schema); 116 | 117 | boolean corporaIndexCreated = es.createCorpusIndex(corpus); 118 | if(!corporaIndexCreated) { 119 | Message msg = new Message("ElasticSearch index failed to be created for corpus: " + corpus.getId()); 120 | return Response.status(400).entity(msg).build(); 121 | } 122 | 123 | return Response.status(201).entity(corpus).build(); 124 | } 125 | } 126 | catch(Exception e) { 127 | Message msg = new Message(e.getMessage()); 128 | return Response.status(400).entity(msg).build(); 129 | } 130 | } 131 | 132 | @GET @Path("{corpusid}") 133 | @Produces(MediaType.APPLICATION_JSON) 134 | public Response getCorpus(@PathParam("corpusid") String corpusid) { 135 | 136 | Corpus corpus = dao.get(corpusid); 137 | if(corpus == null) { 138 | Message msg = new Message("Corpus " + corpusid + " not found"); 139 | return Response.status(404).entity(msg).build(); 140 | } 141 | 142 | if(corpus.getSchemaId() != null) { 143 | Schema schema = schemasDAO.get(corpus.getSchemaId()); 144 | corpus.setSchema(schema); 145 | } 146 | 147 | if(corpus.getTaxonomyId() != null) { 148 | Taxonomy taxonomy = taxonomiesDAO.get(corpus.getTaxonomyId()); 149 | corpus.setTaxonomy(taxonomy); 150 | } 151 | 152 | return Response.status(200).entity(corpus).build(); 153 | } 154 | 155 | @PUT @Path("{corpusid}") 156 | @Produces(MediaType.APPLICATION_JSON) 157 | public Response putCorpus(@PathParam("corpusid") String corpusid, Corpus newCorpus) { 158 | 159 | Corpus corpus = dao.get(corpusid); 160 | if(corpus == null) { 161 | Message msg = new Message("Corpus " + corpusid + " not found"); 162 | return Response.status(404).entity(msg).build(); 163 | } 164 | 165 | Query query = dao.createQuery().filter("_id", new ObjectId(corpusid)); 166 | UpdateOperations ops = dao.createUpdateOperations() 167 | .set("name", newCorpus.getName()) 168 | .set("language", newCorpus.getLanguage()) 169 | .set("schemaId", newCorpus.getSchemaId()) 170 | .set("taxonomyId", newCorpus.getTaxonomyId()); 171 | 172 | dao.update(query, ops); 173 | 174 | 175 | 176 | 177 | return Response.status(200).entity(newCorpus).build(); 178 | } 179 | 180 | @DELETE @Path("{corpusid}") 181 | @Produces(MediaType.APPLICATION_JSON) 182 | public Response deleteCorpus(@PathParam("corpusid") String corpusid) { 183 | 184 | Corpus corpus = dao.get(corpusid); 185 | if(corpus == null) { 186 | Message msg = new Message("Corpus " + corpusid + " not found"); 187 | return Response.status(404).entity(msg).build(); 188 | } 189 | 190 | dao.deleteById(corpusid); 191 | es.deleteCorpusIndex(corpusid); 192 | 193 | return Response.status(204).entity(corpusid).build(); 194 | } 195 | 196 | @GET @Path("{corpusid}/documents") 197 | @Produces(MediaType.APPLICATION_JSON) 198 | @Consumes(MediaType.APPLICATION_JSON) 199 | public Response getDocuments(@PathParam("corpusid") String corpusid, 200 | @DefaultValue("1") @QueryParam("page") int page, 201 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage) { 202 | 203 | Corpus corpus = dao.get(corpusid); 204 | if(corpus == null) { 205 | Message msg = new Message("Corpus " + corpusid + " not found"); 206 | return Response.status(404).entity(msg).build(); 207 | } 208 | 209 | try { 210 | ElasticSearchResponse documents = es.findDocuments(corpusid, page, nPerPage); 211 | 212 | PagedResponse response = new PagedResponse(); 213 | response.setEntries(documents.getResults()); 214 | response.setPage(page); 215 | response.setnPerPage(nPerPage); 216 | response.setTotal(documents.getFound()); 217 | 218 | return Response.ok(response).build(); 219 | 220 | } catch (Exception e) { 221 | e.printStackTrace(); 222 | 223 | Message msg = new Message("Exception: " + e.getMessage()); 224 | return Response.status(404).entity(msg).build(); 225 | } 226 | } 227 | 228 | @POST @Path("{corpusid}/documents") 229 | @Produces(MediaType.APPLICATION_JSON) 230 | @Consumes(MediaType.APPLICATION_JSON) 231 | public Response indexDocument(@PathParam("corpusid") String corpusid, Document document) { 232 | 233 | Corpus corpus = dao.get(corpusid); 234 | if(corpus == null) { 235 | Message msg = new Message("Corpus " + corpusid + " not found"); 236 | return Response.status(404).entity(msg).build(); 237 | } 238 | 239 | Schema schema = schemasDAO.get(corpus.getSchemaId()); 240 | if(schema == null) { 241 | Message msg = new Message("Schema " + corpus.getSchemaId() + " not found"); 242 | return Response.status(404).entity(msg).build(); 243 | } 244 | 245 | es.indexDocument(corpus.getId(), document, schema); 246 | 247 | Message msg = new Message("Document indexed succesfuly"); 248 | return Response.status(201).entity(msg).build(); 249 | 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/DictionariesResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | import javax.ws.rs.Consumes; 8 | import javax.ws.rs.DELETE; 9 | import javax.ws.rs.DefaultValue; 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.PUT; 13 | import javax.ws.rs.Path; 14 | import javax.ws.rs.PathParam; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.QueryParam; 17 | import javax.ws.rs.core.MediaType; 18 | import javax.ws.rs.core.Response; 19 | 20 | import org.iptc.extra.api.datatypes.Message; 21 | import org.iptc.extra.api.datatypes.PagedResponse; 22 | import org.iptc.extra.core.daos.DictionariesDAO; 23 | import org.iptc.extra.core.types.Dictionary; 24 | import org.mongodb.morphia.query.FindOptions; 25 | import org.mongodb.morphia.query.Query; 26 | import org.mongodb.morphia.query.QueryResults; 27 | 28 | /** 29 | * Schemas resource (exposed at "/schemas" path) 30 | */ 31 | @Singleton 32 | @Path("dictionaries") 33 | public class DictionariesResource { 34 | 35 | @Inject 36 | private DictionariesDAO dao; 37 | 38 | 39 | @GET 40 | @Produces(MediaType.APPLICATION_JSON) 41 | public Response getDictionaries( 42 | @DefaultValue("1") @QueryParam("page") int page, 43 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage) { 44 | 45 | 46 | Query query = dao.createQuery(); 47 | QueryResults result = dao.find(query); 48 | 49 | FindOptions options = new FindOptions().skip((page-1)*nPerPage).limit(nPerPage); 50 | 51 | long total = result.count(); 52 | List dictionaries = result.asList(options); 53 | 54 | PagedResponse response = new PagedResponse(); 55 | response.setEntries(dictionaries); 56 | response.setPage(page); 57 | response.setnPerPage(nPerPage); 58 | response.setTotal(total); 59 | 60 | return Response.ok(response).build(); 61 | } 62 | 63 | @POST 64 | @Produces(MediaType.APPLICATION_JSON) 65 | @Consumes(MediaType.APPLICATION_JSON) 66 | public Dictionary postDictionary(Dictionary dictionary) { 67 | return null; 68 | } 69 | 70 | @GET @Path("{dictionaryid}") 71 | @Produces(MediaType.APPLICATION_JSON) 72 | public Response getDictionary(@PathParam("dictionaryid") String dictionaryid) { 73 | 74 | Dictionary dictionary = dao.get(dictionaryid); 75 | if(dictionary == null) { 76 | Message msg = new Message("Dictionary " + dictionaryid + " not found"); 77 | return Response.status(404).entity(msg).build(); 78 | } 79 | 80 | return Response.status(200).entity(dictionary).build(); 81 | } 82 | 83 | @PUT @Path("{dictionaryid}") 84 | @Produces(MediaType.APPLICATION_JSON) 85 | public Dictionary putDictionary(@PathParam("dictionaryid") String dictionaryid) { 86 | return null; 87 | } 88 | 89 | @DELETE @Path("{dictionaryid}") 90 | @Produces(MediaType.APPLICATION_JSON) 91 | public Response deleteDictionary(@PathParam("dictionaryid") String dictionaryid) { 92 | 93 | Dictionary dictionary = dao.get(dictionaryid); 94 | if(dictionary == null) { 95 | Message msg = new Message("Dictionary " + dictionaryid + " not found"); 96 | return Response.status(404).entity(msg).build(); 97 | } 98 | 99 | dao.deleteById(dictionaryid); 100 | return Response.status(204).entity(dictionary).build(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/DocumentsResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.io.IOException; 4 | import java.text.DecimalFormat; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Singleton; 12 | import javax.ws.rs.Consumes; 13 | import javax.ws.rs.DefaultValue; 14 | import javax.ws.rs.GET; 15 | import javax.ws.rs.POST; 16 | import javax.ws.rs.Path; 17 | import javax.ws.rs.PathParam; 18 | import javax.ws.rs.Produces; 19 | import javax.ws.rs.QueryParam; 20 | import javax.ws.rs.core.MediaType; 21 | import javax.ws.rs.core.Response; 22 | 23 | import org.apache.lucene.search.join.ScoreMode; 24 | import org.elasticsearch.index.query.BoolQueryBuilder; 25 | import org.elasticsearch.index.query.QueryBuilder; 26 | 27 | import static org.elasticsearch.index.query.QueryBuilders.*; 28 | 29 | import org.iptc.extra.api.datatypes.DocumentPagedResponse; 30 | import org.iptc.extra.api.datatypes.Message; 31 | import org.iptc.extra.core.daos.CorporaDAO; 32 | import org.iptc.extra.core.daos.RulesDAO; 33 | import org.iptc.extra.core.daos.SchemasDAO; 34 | import org.iptc.extra.core.eql.EQLMapper; 35 | import org.iptc.extra.core.eql.EQLParser; 36 | import org.iptc.extra.core.eql.tree.SyntaxTree; 37 | import org.iptc.extra.core.eql.tree.nodes.Node; 38 | import org.iptc.extra.core.eql.tree.nodes.ReferenceClause; 39 | import org.iptc.extra.core.eql.tree.utils.TreeUtils; 40 | import org.iptc.extra.core.es.ElasticSearchClient; 41 | import org.iptc.extra.core.es.ElasticSearchResponse; 42 | import org.iptc.extra.core.types.Corpus; 43 | import org.iptc.extra.core.types.Rule; 44 | import org.iptc.extra.core.types.Schema; 45 | import org.iptc.extra.core.types.document.Document; 46 | import org.iptc.extra.core.utils.TextUtils; 47 | 48 | /** 49 | * Documents resource (exposed at "/documents" path) 50 | */ 51 | @Singleton 52 | @Path("documents") 53 | public class DocumentsResource { 54 | 55 | @Inject 56 | private ElasticSearchClient es; 57 | 58 | @Inject 59 | private RulesDAO dao; 60 | 61 | @Inject 62 | private CorporaDAO corporaDAO; 63 | 64 | @Inject 65 | private SchemasDAO schemasDAO; 66 | 67 | private EQLMapper mapper = new EQLMapper(); 68 | 69 | @POST 70 | @Produces(MediaType.APPLICATION_JSON) 71 | @Consumes(MediaType.APPLICATION_JSON) 72 | public Response searchDocuments(Rule rule, 73 | @QueryParam("corpus") String corpusId, 74 | @QueryParam("match") String match, 75 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage, 76 | @DefaultValue("1") @QueryParam("page") int page) { 77 | 78 | try { 79 | Corpus corpus = corporaDAO.get(corpusId); 80 | if(corpus == null) { 81 | Message msg = new Message("Cannot find corpus " + corpusId); 82 | return Response.status(400).entity(msg).build(); 83 | } 84 | 85 | String corpusIndex = corpus.getId(); 86 | 87 | Schema schema = schemasDAO.get(corpus.getSchemaId()); 88 | 89 | Rule savedRule = dao.get(rule.getId()); 90 | if(savedRule == null) { 91 | Message msg = new Message("Cannot find rule " + rule.getId()); 92 | return Response.status(404).entity(msg).build(); 93 | } 94 | 95 | QueryBuilder rulesQuery = getRuleQuery(rule, schema); 96 | if(rulesQuery == null) { 97 | Message msg = new Message("CQL to ES translation failed."); 98 | return Response.status(400).entity(msg).build(); 99 | } 100 | 101 | String topicId = savedRule.getTopicId(); 102 | 103 | DocumentPagedResponse response = new DocumentPagedResponse(); 104 | Map counts = getCountAnnotations(rulesQuery, topicId, corpusIndex); 105 | for(Entry count : counts.entrySet()) { 106 | response.addAnnotation(count.getKey(), count.getValue()); 107 | } 108 | 109 | QueryBuilder qb = null; // ES query 110 | QueryBuilder hqb = getHighlightQuery(rule, schema); // ES query used for highlighting 111 | if(match.equals("topicMatches")) { 112 | qb = getTopicQuery(topicId); 113 | hqb = null; 114 | } 115 | else if(match.equals("bothMatches")) { 116 | qb = getRulesAndTopicQuery(rulesQuery, topicId); 117 | } 118 | else if(match.equals("ruleOnlyMatches")) { 119 | qb = getRulesOnlyQuery(rulesQuery, topicId); 120 | } 121 | else if(match.equals("topicOnlyMatches")) { 122 | qb = getTopicOnlyQuery(rulesQuery, topicId); 123 | hqb = null; 124 | } 125 | else { 126 | // rule matches - default option 127 | qb = rulesQuery; 128 | } 129 | 130 | ElasticSearchResponse results = es.findDocuments(qb, corpusIndex, page, nPerPage, schema, hqb); 131 | response.setEntries(results.getResults()); 132 | 133 | response.setTotal(results.getFound()); 134 | response.setnPerPage(nPerPage); 135 | response.setPage(page); 136 | 137 | return Response.status(200).entity(response).build(); 138 | 139 | } catch (Exception e) { 140 | e.printStackTrace(); 141 | Message msg = new Message(e.getMessage()); 142 | return Response.status(400).entity(msg).build(); 143 | } 144 | } 145 | 146 | private Map getCountAnnotations(QueryBuilder rulesQb, String topicId, String corpusIndex) throws IOException { 147 | Map counts = new HashMap(); 148 | 149 | long allDocuments = es.countDocuments(matchAllQuery(), corpusIndex); 150 | 151 | long ruleMatches = es.countDocuments(rulesQb, corpusIndex); 152 | 153 | QueryBuilder topicsQuery = getTopicQuery(topicId); 154 | long topicMatches = es.countDocuments(topicsQuery, corpusIndex); 155 | 156 | QueryBuilder ruleAndTopicQuery = getRulesAndTopicQuery(rulesQb, topicId); 157 | long bothMatches = es.countDocuments(ruleAndTopicQuery, corpusIndex); 158 | 159 | QueryBuilder onlyRuleQuery = getRulesOnlyQuery(rulesQb, topicId); 160 | long ruleOnlyMatches = es.countDocuments(onlyRuleQuery, corpusIndex); 161 | 162 | QueryBuilder onlyTopicQuery = getTopicOnlyQuery(rulesQb, topicId); 163 | long topicOnlyMatches = es.countDocuments(onlyTopicQuery, corpusIndex); 164 | 165 | counts.put("ruleMatches", ruleMatches); 166 | counts.put("topicMatches", topicMatches); 167 | counts.put("bothMatches", bothMatches); 168 | counts.put("ruleOnlyMatches", ruleOnlyMatches); 169 | counts.put("topicOnlyMatches", topicOnlyMatches); 170 | 171 | Double precision = ruleMatches == 0 ? 0 : (double) bothMatches / (double) ruleMatches; 172 | Double recall = topicMatches == 0 ? 0 : (double) bothMatches / (double) topicMatches; 173 | 174 | 175 | Double accuracy = allDocuments == 0 ? 0 : (double)(allDocuments - ruleOnlyMatches - topicOnlyMatches) / (double) allDocuments; 176 | 177 | DecimalFormat formatter = new DecimalFormat("###.###"); 178 | 179 | counts.put("precision", formatter.format(precision)); 180 | counts.put("recall", formatter.format(recall)); 181 | counts.put("accuracy", formatter.format(accuracy)); 182 | 183 | return counts; 184 | } 185 | 186 | private QueryBuilder getRuleQuery(Rule rule, Schema schema) { 187 | try { 188 | // parse rule 189 | String cql = rule.getQuery(); 190 | cql = TextUtils.clean(rule.getQuery()); 191 | SyntaxTree syntaxTree = EQLParser.parse(cql); 192 | if(syntaxTree.hasErrors() || syntaxTree.getRootNode() == null) { 193 | return null; 194 | } 195 | 196 | Node root = syntaxTree.getRootNode(); 197 | List references = TreeUtils.getReferences(root); 198 | if(!references.isEmpty()) { 199 | for(ReferenceClause reference : references) { 200 | String refRuleId = reference.getRuleId(); 201 | Rule refRule = dao.get(refRuleId); 202 | if(refRule != null) { 203 | reference.setRule(refRule); 204 | 205 | String query = refRule.getQuery(); 206 | reference.setRuleSyntaxTree(EQLParser.parse(query)); 207 | } 208 | } 209 | } 210 | 211 | QueryBuilder qb = mapper.toElasticSearchQuery(root, schema); 212 | return qb; 213 | } 214 | catch(Exception e) { 215 | return null; 216 | } 217 | } 218 | 219 | private QueryBuilder getHighlightQuery(Rule rule, Schema schema) { 220 | try { 221 | // parse rule 222 | String cql = rule.getQuery(); 223 | cql = TextUtils.clean(rule.getQuery()); 224 | SyntaxTree syntaxTree = EQLParser.parse(cql); 225 | if(syntaxTree.hasErrors() || syntaxTree.getRootNode() == null) { 226 | return null; 227 | } 228 | 229 | Node root = syntaxTree.getRootNode(); 230 | 231 | QueryBuilder hqb = mapper.toElasticSearchHighlight(root, schema); 232 | return hqb; 233 | } 234 | catch(Exception e) { 235 | return null; 236 | } 237 | } 238 | 239 | private QueryBuilder getTopicQuery(String topicId) { 240 | 241 | BoolQueryBuilder bqb = boolQuery(); 242 | bqb.must(termQuery("topics.topicId", topicId)); 243 | bqb.must(termQuery("topics.exclude", false)); 244 | bqb.must(boolQuery() 245 | .should(termQuery("topics.association", "direct")) 246 | .should(termQuery("topics.association", "userdefined"))); 247 | 248 | QueryBuilder qb = nestedQuery("topics", bqb, ScoreMode.Total); 249 | return qb; 250 | } 251 | 252 | private QueryBuilder getRulesAndTopicQuery(QueryBuilder rulesQb, String topicId) { 253 | BoolQueryBuilder qb = boolQuery(); 254 | qb.must(rulesQb); 255 | 256 | QueryBuilder topicQb = getTopicQuery(topicId); 257 | qb.must(topicQb); 258 | 259 | return qb; 260 | } 261 | 262 | private QueryBuilder getRulesOnlyQuery(QueryBuilder rulesQb, String topicId) { 263 | BoolQueryBuilder qb = boolQuery(); 264 | qb.must(rulesQb); 265 | 266 | QueryBuilder topicQb = getTopicQuery(topicId); 267 | qb.mustNot(topicQb); 268 | 269 | return qb; 270 | } 271 | 272 | private QueryBuilder getTopicOnlyQuery(QueryBuilder rulesQb, String topicId) { 273 | 274 | BoolQueryBuilder qb = boolQuery(); 275 | 276 | QueryBuilder topicQb = getTopicQuery(topicId); 277 | qb.must(topicQb); 278 | 279 | qb.mustNot(rulesQb); 280 | 281 | return qb; 282 | } 283 | 284 | @GET @Path("{documentid}") 285 | @Produces(MediaType.APPLICATION_JSON) 286 | @Consumes(MediaType.APPLICATION_JSON) 287 | public Response getDocument(@PathParam("documentid") String documentid, @QueryParam("corpusid") String corpusid) { 288 | 289 | Corpus corpus = corporaDAO.get(corpusid); 290 | if(corpus == null) { 291 | Message msg = new Message("Corpus " + corpusid + " not found"); 292 | return Response.status(404).entity(msg).build(); 293 | } 294 | 295 | Schema schema = schemasDAO.get(corpus.getSchemaId()); 296 | if(schema == null) { 297 | Message msg = new Message("Schema " + corpus.getSchemaId() + " not found"); 298 | return Response.status(404).entity(msg).build(); 299 | } 300 | 301 | try { 302 | Document document = es.getDocument(documentid, corpus.getId(), schema); 303 | if(document == null) { 304 | Message msg = new Message("Document " + documentid + " not found"); 305 | return Response.status(404).entity(msg).build(); 306 | } 307 | 308 | return Response.status(200).entity(document).build(); 309 | 310 | } catch (IOException e) { 311 | Message msg = new Message("Exception for document " + documentid + ": " + e.getMessage()); 312 | return Response.status(400).entity(msg).build(); 313 | } 314 | 315 | } 316 | 317 | } -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/SchemasResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | import javax.ws.rs.Consumes; 8 | import javax.ws.rs.DELETE; 9 | import javax.ws.rs.DefaultValue; 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.PUT; 13 | import javax.ws.rs.Path; 14 | import javax.ws.rs.PathParam; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.QueryParam; 17 | import javax.ws.rs.core.MediaType; 18 | import javax.ws.rs.core.Response; 19 | 20 | import org.bson.types.ObjectId; 21 | import org.iptc.extra.api.datatypes.Message; 22 | import org.iptc.extra.api.datatypes.PagedResponse; 23 | import org.iptc.extra.core.daos.SchemasDAO; 24 | import org.iptc.extra.core.types.Schema; 25 | import org.mongodb.morphia.Key; 26 | import org.mongodb.morphia.query.FindOptions; 27 | import org.mongodb.morphia.query.Query; 28 | import org.mongodb.morphia.query.QueryResults; 29 | import org.mongodb.morphia.query.UpdateOperations; 30 | 31 | /** 32 | * Schemas resource (exposed at "/schemas" path) 33 | */ 34 | @Singleton 35 | @Path("schemas") 36 | public class SchemasResource { 37 | 38 | @Inject 39 | private SchemasDAO dao; 40 | 41 | @GET 42 | @Produces(MediaType.APPLICATION_JSON) 43 | public Response getSchemas( 44 | @DefaultValue("1") @QueryParam("page") int page, 45 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage) { 46 | 47 | Query query = dao.createQuery(); 48 | QueryResults result = dao.find(query); 49 | 50 | FindOptions options = new FindOptions().skip((page-1)*nPerPage).limit(nPerPage); 51 | 52 | long total = result.count(); 53 | List schemas = result.asList(options); 54 | 55 | PagedResponse response = new PagedResponse(); 56 | response.setEntries(schemas); 57 | response.setPage(page); 58 | response.setnPerPage(nPerPage); 59 | response.setTotal(total); 60 | 61 | return Response.ok(response).build(); 62 | } 63 | 64 | @POST 65 | @Produces(MediaType.APPLICATION_JSON) 66 | @Consumes(MediaType.APPLICATION_JSON) 67 | public Response postSchema(Schema schema) { 68 | String id = schema.getId(); 69 | if(id != null && dao.exists(id)) { 70 | Message msg = new Message("Conflict. Schema " + id + " already exists."); 71 | return Response.status(409).entity(msg).build(); 72 | } 73 | else { 74 | Key createdSchemaKey = dao.save(schema); 75 | schema.setId(createdSchemaKey.getId().toString()); 76 | 77 | return Response.status(201).entity(schema).build(); 78 | } 79 | } 80 | 81 | @GET @Path("{schemaid}") 82 | @Produces(MediaType.APPLICATION_JSON) 83 | public Response getSchema(@PathParam("schemaid") String schemaid) { 84 | Schema schema = dao.get(schemaid); 85 | if(schema == null) { 86 | Message msg = new Message("Schema " + schema + " not found"); 87 | return Response.status(404).entity(msg).build(); 88 | } 89 | 90 | return Response.status(200).entity(schema).build(); 91 | } 92 | 93 | @PUT @Path("{schemaid}") 94 | @Produces(MediaType.APPLICATION_JSON) 95 | public Response putSchema(@PathParam("schemaid") String schemaid, Schema newSchema) { 96 | Schema schema = dao.get(schemaid); 97 | if(schema == null) { 98 | Message msg = new Message("Schema " + schema + " not found"); 99 | return Response.status(404).entity(msg).build(); 100 | } 101 | 102 | Query query = dao.createQuery().filter("_id", new ObjectId(schemaid)); 103 | UpdateOperations ops = dao.createUpdateOperations() 104 | .set("name", newSchema.getName()) 105 | .set("language", newSchema.getLanguage()) 106 | .set("fields", newSchema.getFields()); 107 | 108 | dao.update(query, ops); 109 | 110 | return Response.status(201).entity(newSchema).build(); 111 | } 112 | 113 | @DELETE @Path("{schemaid}") 114 | @Produces(MediaType.APPLICATION_JSON) 115 | public Response deleteSchema(@PathParam("schemaid") String schemaid) { 116 | try { 117 | Schema schema = dao.get(schemaid); 118 | if(schema == null) { 119 | Message msg = new Message("Schema " + schema + " not found"); 120 | return Response.status(404).entity(msg).build(); 121 | } 122 | 123 | dao.deleteById(schemaid); 124 | 125 | return Response.status(204).entity(schema).build(); 126 | } 127 | catch(Exception e) { 128 | return Response.status(400).entity(e.getMessage()).build(); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/TaxonomyResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | import javax.ws.rs.Consumes; 8 | import javax.ws.rs.DELETE; 9 | import javax.ws.rs.DefaultValue; 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.PUT; 13 | import javax.ws.rs.Path; 14 | import javax.ws.rs.PathParam; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.QueryParam; 17 | import javax.ws.rs.core.MediaType; 18 | import javax.ws.rs.core.Response; 19 | 20 | import org.iptc.extra.api.datatypes.Message; 21 | import org.iptc.extra.api.datatypes.PagedResponse; 22 | import org.iptc.extra.core.daos.TaxonomiesDAO; 23 | import org.iptc.extra.core.daos.TopicsDAO; 24 | import org.iptc.extra.core.types.Taxonomy; 25 | import org.iptc.extra.core.types.Topic; 26 | import org.mongodb.morphia.Key; 27 | import org.mongodb.morphia.query.FindOptions; 28 | import org.mongodb.morphia.query.Query; 29 | import org.mongodb.morphia.query.QueryResults; 30 | import org.mongodb.morphia.query.UpdateOperations; 31 | 32 | import com.mongodb.WriteResult; 33 | 34 | @Singleton 35 | @Path("taxonomies") 36 | public class TaxonomyResource { 37 | 38 | 39 | @Inject 40 | private TopicsDAO topicsDao; 41 | 42 | @Inject 43 | private TaxonomiesDAO taxonomiesDao; 44 | 45 | @GET 46 | @Produces(MediaType.APPLICATION_JSON) 47 | public Response getTaxonomies( 48 | @DefaultValue("1") @QueryParam("page") int page, 49 | @DefaultValue("20") @QueryParam("nPerPage") int nPerPage) { 50 | 51 | FindOptions options = new FindOptions().skip((page-1)*nPerPage).limit(nPerPage); 52 | 53 | QueryResults result = taxonomiesDao.find(); 54 | 55 | long total = result.count(); 56 | List taxonomies = result.asList(options); 57 | 58 | for(Taxonomy taxonomy : taxonomies) { 59 | long topics = topicsDao.createQuery().filter("taxonomyId", taxonomy.getId()).count(); 60 | taxonomy.setTopics(topics); 61 | } 62 | 63 | PagedResponse response = new PagedResponse(); 64 | response.setEntries(taxonomies); 65 | response.setTotal(total); 66 | response.setPage(page); 67 | response.setnPerPage(nPerPage); 68 | 69 | return Response.status(200).entity(response).build(); 70 | } 71 | 72 | @POST 73 | @Produces(MediaType.APPLICATION_JSON) 74 | @Consumes(MediaType.APPLICATION_JSON) 75 | public Response postTaxonomy(Taxonomy taxonomy) { 76 | String id = taxonomy.getId(); 77 | if(id != null && taxonomiesDao.exists(id)) { 78 | Message msg = new Message("Conflict. Taxonomy " + id + " already exists."); 79 | return Response.status(409).entity(msg).build(); 80 | } 81 | else { 82 | Key createdTaxonomyKey = taxonomiesDao.save(taxonomy); 83 | taxonomy.setId(createdTaxonomyKey.getId().toString()); 84 | 85 | return Response.status(201).entity(taxonomy).build(); 86 | } 87 | } 88 | 89 | @GET @Path("{taxonomyid}") 90 | @Produces(MediaType.APPLICATION_JSON) 91 | public Response getTaxonomy(@PathParam("taxonomyid") String taxonomyid) { 92 | 93 | Taxonomy taxonomy = taxonomiesDao.get(taxonomyid); 94 | if(taxonomy == null) { 95 | Message msg = new Message("Taxonomy " + taxonomy + " not found"); 96 | return Response.status(404).entity(msg).build(); 97 | } 98 | 99 | Query query = topicsDao.createQuery().filter("taxonomyId", taxonomyid); 100 | long topics = topicsDao.count(query); 101 | taxonomy.setTopics(topics); 102 | 103 | return Response.status(200).entity(taxonomy).build(); 104 | } 105 | 106 | @PUT @Path("{taxonomyid}") 107 | @Produces(MediaType.APPLICATION_JSON) 108 | public Response putTaxonomy(@PathParam("taxonomyid") String taxonomyid, Taxonomy taxonomy) { 109 | return Response.status(200).entity(taxonomy).build(); 110 | } 111 | 112 | 113 | @DELETE @Path("{taxonomyid}") 114 | @Produces(MediaType.APPLICATION_JSON) 115 | public Response deleteTaxonomy(@PathParam("taxonomyid") String taxonomyid) { 116 | 117 | Taxonomy taxonomy = taxonomiesDao.get(taxonomyid); 118 | if(taxonomy == null) { 119 | Message msg = new Message("Taxonomy " + taxonomyid + " not found"); 120 | return Response.status(404).entity(msg).build(); 121 | } 122 | 123 | WriteResult r = taxonomiesDao.deleteById(taxonomyid); 124 | if(r.getN() == 0) { 125 | Message msg = new Message("Taxonomy " + taxonomyid + " failed to be deleted"); 126 | return Response.status(404).entity(msg).build(); 127 | } 128 | 129 | Query query = topicsDao.createQuery().filter("taxonomyId", taxonomyid); 130 | r = topicsDao.deleteByQuery(query); 131 | if(r.getN() == 0) { 132 | Message msg = new Message("Taxonomy " + taxonomyid + " topics failed to be deleted"); 133 | return Response.status(404).entity(msg).build(); 134 | } 135 | 136 | return Response.status(204).entity(taxonomy).build(); 137 | } 138 | 139 | @GET @Path("{taxonomyid}/topics") 140 | @Produces(MediaType.APPLICATION_JSON) 141 | public Response getTaxonomyTopics(@PathParam("taxonomyid") String taxonomyid, @QueryParam("q") String q, 142 | @DefaultValue("1") @QueryParam("page") int page, @DefaultValue("20") @QueryParam("nPerPage") int nPerPage) { 143 | 144 | try { 145 | FindOptions options = new FindOptions().skip((page-1)*nPerPage).limit(nPerPage); 146 | 147 | Query query = topicsDao.createQuery().filter("taxonomyId", taxonomyid); 148 | query = query.order("topicId"); 149 | 150 | if(q != null && !q.equals("")) { 151 | query.or( 152 | query.criteria("name").contains(q), 153 | query.criteria("topicId").contains(q) 154 | ); 155 | } 156 | 157 | QueryResults result = topicsDao.find(query); 158 | 159 | long total = result.count(); 160 | List topics = result.asList(options); 161 | 162 | PagedResponse response = new PagedResponse(); 163 | response.setEntries(topics); 164 | response.setPage(page); 165 | response.setnPerPage(nPerPage); 166 | response.setTotal(total); 167 | 168 | return Response.status(200).entity(response).build(); 169 | } 170 | catch(Exception e) { 171 | Message msg = new Message(e.getMessage()); 172 | return Response.status(400).entity(msg).build(); 173 | } 174 | } 175 | 176 | @POST @Path("{taxonomyid}/topics") 177 | @Produces(MediaType.APPLICATION_JSON) 178 | public Response postTaxonomyTopic(@PathParam("taxonomyid") String taxonomyid, Topic topic) { 179 | 180 | try { 181 | topic.setId(taxonomyid + "#" + topic.getTopicId()); 182 | topic.setLabel(topic.getName() + " (" + topic.getTopicId() + ")"); 183 | topic.setTaxonomyId(taxonomyid); 184 | 185 | topicsDao.save(topic); 186 | 187 | return Response.status(201).entity(topic).build(); 188 | } 189 | catch(Exception e) { 190 | Message msg = new Message(e.getMessage()); 191 | return Response.status(400).entity(msg).build(); 192 | } 193 | } 194 | 195 | @GET @Path("{taxonomyid}/topics/{topicid}") 196 | @Produces(MediaType.APPLICATION_JSON) 197 | public Response getTaxonomyTopic( 198 | @PathParam("taxonomyid") String taxonomyid, 199 | @PathParam("topicid") String topicId, 200 | Topic newTopic) { 201 | 202 | try { 203 | Topic topic = topicsDao.get(topicId, taxonomyid); 204 | if(topic == null) { 205 | Message msg = new Message("Topic " + topicId + " not found"); 206 | return Response.status(404).entity(msg).build(); 207 | } 208 | 209 | return Response.status(201).entity(topic).build(); 210 | } 211 | catch(Exception e) { 212 | Message msg = new Message(e.getMessage()); 213 | return Response.status(400).entity(msg).build(); 214 | } 215 | } 216 | 217 | @PUT @Path("{taxonomyid}/topics/{topicid}") 218 | @Produces(MediaType.APPLICATION_JSON) 219 | public Response putTaxonomyTopic( 220 | @PathParam("taxonomyid") String taxonomyid, 221 | @PathParam("topicid") String topicId, 222 | Topic newTopic) { 223 | 224 | try { 225 | Topic topic = topicsDao.get(topicId, taxonomyid); 226 | if(topic == null) { 227 | Message msg = new Message("Topic " + topicId + " not found"); 228 | return Response.status(404).entity(msg).build(); 229 | } 230 | 231 | if(topicId.equals(newTopic.getTopicId())) { 232 | Query query = topicsDao.createQuery().filter("topicId", topicId).filter("taxonomyId", taxonomyid); 233 | UpdateOperations ops = topicsDao.createUpdateOperations() 234 | .set("name", newTopic.getName()) 235 | .set("definition", newTopic.getDefinition()); 236 | 237 | topicsDao.update(query, ops); 238 | 239 | topic = topicsDao.get(topicId, taxonomyid); 240 | return Response.status(201).entity(topic).build(); 241 | } 242 | else { 243 | newTopic.setId(taxonomyid + "#" + newTopic.getTopicId()); 244 | newTopic.setLabel(topic.getName() + " (" + newTopic.getTopicId() + ")"); 245 | newTopic.setTaxonomyId(taxonomyid); 246 | topicsDao.save(newTopic); 247 | 248 | topicsDao.delete(topicId, taxonomyid); 249 | 250 | return Response.status(201).entity(newTopic).build(); 251 | } 252 | 253 | } 254 | catch(Exception e) { 255 | Message msg = new Message(e.getMessage()); 256 | return Response.status(400).entity(msg).build(); 257 | } 258 | } 259 | 260 | @DELETE @Path("{taxonomyid}/topics/{topicid}") 261 | @Produces(MediaType.APPLICATION_JSON) 262 | public Response deleteTaxonomyTopic( 263 | @PathParam("taxonomyid") String taxonomyid, 264 | @PathParam("topicid") String topicId) { 265 | 266 | try { 267 | Topic topic = topicsDao.get(topicId, taxonomyid); 268 | if(topic == null) { 269 | Message msg = new Message("Topic " + topicId + " not found"); 270 | return Response.status(404).entity(msg).build(); 271 | } 272 | 273 | WriteResult r = topicsDao.delete(topicId, taxonomyid); 274 | if(r.getN() == 0) { 275 | Message msg = new Message("Topic " + topicId + " failed to be deleted"); 276 | return Response.status(404).entity(msg).build(); 277 | } 278 | 279 | return Response.status(204).entity(topic).build(); 280 | } 281 | catch(Exception e) { 282 | Message msg = new Message("Exception: " + e.getMessage()); 283 | return Response.status(400).entity(msg).build(); 284 | } 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /api/extra-api/src/main/java/org/iptc/extra/api/resources/ValidationsResource.java: -------------------------------------------------------------------------------- 1 | package org.iptc.extra.api.resources; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Singleton; 10 | import javax.ws.rs.Consumes; 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.Path; 13 | import javax.ws.rs.PathParam; 14 | import javax.ws.rs.Produces; 15 | import javax.ws.rs.QueryParam; 16 | import javax.ws.rs.core.GenericEntity; 17 | import javax.ws.rs.core.MediaType; 18 | import javax.ws.rs.core.Response; 19 | 20 | import org.apache.commons.lang3.StringUtils; 21 | 22 | import org.elasticsearch.index.query.QueryBuilder; 23 | import org.iptc.extra.api.datatypes.Message; 24 | import org.iptc.extra.core.daos.CorporaDAO; 25 | import org.iptc.extra.core.daos.RulesDAO; 26 | import org.iptc.extra.core.daos.SchemasDAO; 27 | import org.iptc.extra.core.eql.EQLMapper; 28 | import org.iptc.extra.core.eql.EQLParser; 29 | import org.iptc.extra.core.eql.tree.SyntaxTree; 30 | import org.iptc.extra.core.eql.tree.nodes.ErrorMessageNode; 31 | import org.iptc.extra.core.eql.tree.nodes.Node; 32 | import org.iptc.extra.core.eql.tree.nodes.ReferenceClause; 33 | import org.iptc.extra.core.eql.tree.utils.TreeUtils; 34 | import org.iptc.extra.core.eql.tree.visitor.EQLValidator; 35 | import org.iptc.extra.core.types.Corpus; 36 | import org.iptc.extra.core.types.Rule; 37 | import org.iptc.extra.core.types.Schema; 38 | import org.iptc.extra.core.utils.TextUtils; 39 | 40 | 41 | @Singleton 42 | @Path("validations") 43 | public class ValidationsResource { 44 | 45 | private EQLMapper mapper = new EQLMapper(); 46 | 47 | @Inject 48 | private RulesDAO rulesDAO; 49 | 50 | @Inject 51 | private CorporaDAO corporaDAO; 52 | 53 | @Inject 54 | private SchemasDAO schemasDAO; 55 | 56 | @POST 57 | @Produces(MediaType.APPLICATION_JSON) 58 | @Consumes(MediaType.APPLICATION_JSON) 59 | public Response postRuleValidation(Rule rule, @QueryParam("corpus") String corpusId) { 60 | try { 61 | 62 | String query = rule.getQuery(); 63 | 64 | Map response = new HashMap(); 65 | response.put("es_dsl", ""); 66 | response.put("tree", ""); 67 | response.put("html", ""); 68 | response.put("query", query); 69 | 70 | query = TextUtils.clean(query); 71 | rule.setQuery(query); 72 | 73 | SyntaxTree syntaxTree = EQLParser.parse(query); 74 | Node root = syntaxTree.getRootNode(); 75 | 76 | StringBuffer message = new StringBuffer(); 77 | 78 | Corpus corpus = corporaDAO.get(corpusId); 79 | Schema schema = schemasDAO.get(corpus.getSchemaId()); 80 | 81 | if(syntaxTree.hasErrors() || syntaxTree.getRootNode() == null) { 82 | response.put("valid", "false"); 83 | message.append(StringUtils.join(syntaxTree.getErrors(), "
")); 84 | } 85 | else { 86 | List invalidNodes = EQLValidator.validate(root, schema); 87 | if(invalidNodes.isEmpty()) { 88 | response.put("valid", "true"); 89 | } 90 | else { 91 | response.put("valid", "false"); 92 | message.append("
The rule has invalid operators/relations:
- " + StringUtils.join(invalidNodes, "
- ")); 93 | } 94 | 95 | List references = TreeUtils.getReferences(root); 96 | if(!references.isEmpty()) { 97 | for(ReferenceClause reference : references) { 98 | String refRuleId = reference.getRuleId(); 99 | Rule refRule = rulesDAO.get(refRuleId); 100 | if(refRule == null) { 101 | response.put("valid", "false"); 102 | reference.setValid(false); 103 | message.append("
Rule " + refRuleId + " does not exist."); 104 | } 105 | else { 106 | reference.setRule(refRule); 107 | TreeUtils.validateReferenceRule(reference, schema); 108 | } 109 | } 110 | } 111 | 112 | Set unmatchedIndices = TreeUtils.validateSchema(root, schema); 113 | if(!unmatchedIndices.isEmpty()) { 114 | response.put("valid", "false"); 115 | message.append("
Cannot match " + schema.getName() + " due to invalid indices: " + unmatchedIndices); 116 | } 117 | } 118 | 119 | if(root != null) { 120 | if(response.get("valid").equals("true")) { 121 | QueryBuilder qb = mapper.toElasticSearchQuery(root, schema); 122 | if(qb != null) { 123 | String esDSL = "{ \"query\": " + qb.toString() + "}"; 124 | response.put("es_dsl", esDSL); 125 | } 126 | else { 127 | System.out.println("ES QUERY IS NULL!!!"); 128 | } 129 | } 130 | 131 | String htmlTaggedCql = mapper.toHtml(root, "div"); 132 | if(htmlTaggedCql != null) { 133 | response.put("html", htmlTaggedCql); 134 | } 135 | 136 | query = mapper.toString(root, "
", " "); 137 | if(query != null) { 138 | response.put("query", query); 139 | } 140 | 141 | String jstree = mapper.toJSTree(root); 142 | if(jstree != null) { 143 | response.put("tree", jstree); 144 | } 145 | 146 | Set indices = TreeUtils.getIndices(root); 147 | response.put("indices", indices); 148 | 149 | } 150 | 151 | response.put("message", message.toString()); 152 | response.put("rule", rule); 153 | 154 | GenericEntity> entity = new GenericEntity>(response) {}; 155 | return Response.ok().entity(entity).build(); 156 | } 157 | catch(Exception e) { 158 | e.printStackTrace(); 159 | Message error = new Message(e.getMessage()); 160 | return Response.status(400).entity(error).build(); 161 | } 162 | } 163 | 164 | @POST @Path("{schemaid}") 165 | @Produces(MediaType.APPLICATION_JSON) 166 | @Consumes(MediaType.APPLICATION_JSON) 167 | public Response postRuleValidation(@PathParam("schemaid") String schemaid, Rule rule) { 168 | 169 | return null; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /api/extra-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | mongodb.host = mongodb 2 | mongodb.port = 27017 3 | mongodb.database = extra 4 | mongodb.username = 5 | mongodb.password = 6 | es.host = elasticsearch 7 | es.port = 9300 8 | base.uri = http://0.0.0.0:8888/extra/api -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | mongodb: 5 | image: mongo:3.2.11 6 | volumes: 7 | - :/data/db 8 | restart: always 9 | networks: 10 | esnet: 11 | aliases: 12 | - mongodb 13 | 14 | elasticsearch: 15 | image: docker.elastic.co/elasticsearch/elasticsearch:5.2.0 16 | ports: 17 | - 9200:9200 18 | - 9300:9300 19 | volumes: 20 | - :/usr/share/elasticsearch/data 21 | restart: always 22 | environment: 23 | http.host: 0.0.0.0 24 | transport.host: 0.0.0.0 25 | discovery.zen.minimum_master_nodes: 1 26 | cluster.name: elasticsearch 27 | script.max_compilations_per_minute: 200 28 | xpack.security.enabled: 'false' 29 | xpack.monitoring.enabled: 'false' 30 | ES_JAVA_OPTS: -Xms1024m -Xmx1024m 31 | networks: 32 | esnet: 33 | aliases: 34 | - elasticsearch 35 | 36 | documentsapi: 37 | build: 38 | context: documents-api 39 | volumes: 40 | - ./documents-api:/api 41 | command: python3 api.py 42 | ports: 43 | - 5000:5000 44 | restart: always 45 | networks: 46 | esnet: 47 | aliases: 48 | - documentsapi 49 | 50 | api: 51 | build: 52 | context: api 53 | ports: 54 | - 8888:8888 55 | restart: always 56 | networks: 57 | esnet: 58 | aliases: 59 | - api 60 | 61 | ui: 62 | build: 63 | context: ui 64 | ports: 65 | - 80:80 66 | volumes: 67 | - ./ui/html:/var/www/site 68 | restart: always 69 | networks: 70 | esnet: 71 | aliases: 72 | - ui 73 | 74 | networks: 75 | esnet: 76 | driver: bridge 77 | -------------------------------------------------------------------------------- /documents-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.4.6 2 | 3 | MAINTAINER Manos Schinas 4 | 5 | COPY . /api 6 | WORKDIR /api 7 | 8 | RUN pip install -r requirements.txt 9 | -------------------------------------------------------------------------------- /documents-api/requirements.txt: -------------------------------------------------------------------------------- 1 | elasticsearch==5.2.0 2 | elasticsearch-dsl==5.1.0 3 | flask>=1.0.0 4 | Flask-RESTful==0.3.7 5 | Flask-Cors==3.0.10 6 | pymongo==3.1.1 7 | -------------------------------------------------------------------------------- /documents-api/sections.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import os 3 | import json 4 | from elasticsearch import Elasticsearch 5 | 6 | class Node(object): 7 | tab = '\t' 8 | 9 | def __init__(self, data, delimiter, parent_id=None, level=0): 10 | self.id = str(uuid.uuid3(uuid.NAMESPACE_DNS, data)) 11 | self.data = data 12 | self.delimiter = delimiter 13 | self.parent_id = parent_id 14 | self.level = level 15 | 16 | parts = data.split(delimiter) 17 | self.label = parts[-1] 18 | 19 | self.children = [] 20 | 21 | def walk(self): 22 | yield self 23 | for child in self.children: 24 | for n in child.walk(): 25 | yield n 26 | 27 | def add_child(self, obj): 28 | if not self.child_exists(obj): 29 | obj.parent_id = self.id 30 | obj.level = self.level + 1 31 | self.children.append(obj) 32 | 33 | def print_node(self, i=0): 34 | print((self.tab * i) + self.data) 35 | i += 1 36 | for child in self.children: 37 | child.print_node(i) 38 | 39 | def get_by_id(self, id): 40 | if self.id == id: 41 | return self 42 | else: 43 | for child in self.children: 44 | found = child.get_by_id(id) 45 | if found is not None: 46 | return found 47 | return None 48 | 49 | def find(self, d): 50 | if self.data == d: 51 | return self 52 | else: 53 | for child in self.children: 54 | found = child.find(d) 55 | if found is not None: 56 | return found 57 | return None 58 | 59 | def child_exists(self, child_node): 60 | for child in self.children: 61 | if child.data == child_node.data: 62 | return True 63 | return False 64 | 65 | def to_json(self): 66 | json_section = { 67 | 'id': self.id, 68 | 'section_name': self.data, 69 | 'label': self.label, 70 | 'level': self.level, 71 | 'parent': self.parent_id 72 | } 73 | 74 | if len(self.children) > 0: 75 | ch = list() 76 | for child in self.children: 77 | ch.append(child.to_json()) 78 | json_section['children'] = ch 79 | return json_section 80 | 81 | def collect_names(self, names_to_collect): 82 | parts = self.data.split(self.delimiter) 83 | for part in parts: 84 | names_to_collect.add(part) 85 | for child in self.children: 86 | child.collect_names(names_to_collect) 87 | 88 | 89 | def create_sections_mapping(es: Elasticsearch, index_name, update=True): 90 | properties = { 91 | 'properties': { 92 | 'id': {'type': 'keyword'}, 93 | 'parent': {'type': 'keyword'}, 94 | 'label': {'type': 'keyword'}, 95 | 'level': {'type': 'long'}, 96 | 'section_name': { 97 | 'type': 'text', 98 | 'analyzer': 'path-analyzer' 99 | } 100 | } 101 | } 102 | request_body = { 103 | 'settings': { 104 | 'analysis': { 105 | 'analyzer': { 106 | 'path-analyzer': { 107 | 'type': 'custom', 108 | 'tokenizer': 'path-tokenizer', 109 | 'filter': 'lowercase' 110 | } 111 | }, 112 | 'tokenizer': { 113 | 'path-tokenizer': { 114 | 'type': 'path_hierarchy', 115 | 'delimiter': '/' 116 | } 117 | } 118 | } 119 | }, 120 | 'mappings': { 121 | 'sections': properties 122 | } 123 | } 124 | 125 | if update: 126 | response = es.indices.put_mapping(index=index_name, doc_type='sections', body=properties) 127 | print('Update: ', response) 128 | else: 129 | response = es.indices.create(index=index_name, body=request_body) 130 | print('Creation: ', response) 131 | 132 | def get_tokens(es, string, delimiter): 133 | body = { 134 | 'tokenizer': { 135 | 'type':'path_hierarchy', 136 | 'delimiter': '/' 137 | }, 138 | 'text': string 139 | } 140 | tokens = list() 141 | response = es.indices.analyze(body=body) 142 | for token in response['tokens']: 143 | tokens.append(token['token']) 144 | return tokens 145 | 146 | 147 | def index_sections(es: Elasticsearch, root: Node, index_name): 148 | body = [] 149 | for node in root.walk(): 150 | json_node = node.to_json() 151 | if 'children' in json_node: 152 | json_node.pop('children') 153 | body.append({'index': {'_index': index_name, '_type': 'sections', '_id': json_node['id']}}) 154 | body.append(json_node) 155 | es.bulk(body=body, index=index_name, doc_type='sections') 156 | 157 | 158 | if __name__ == '__main__': 159 | corpora = [ 160 | ['/disk1_data/EXTRA/Reuters dataset/json_v2/', 'reuters'], 161 | ['/disk1_data/EXTRA/APA dataset/json_v2/', 'apa'] 162 | ] 163 | 164 | es = Elasticsearch() 165 | for i in 0, 1: 166 | print(corpora[i][1], 'corpus') 167 | create_sections_mapping(es, index_name=corpora[i][1]) 168 | root = Node('sections', '/'); 169 | slug_lines = set() 170 | for filename in os.listdir(corpora[i][0]): 171 | file = open( corpora[i][0] + filename, "r") 172 | json_article = json.load(file) 173 | slug_lines.add(json_article['slugline']) 174 | for slugline in slug_lines: 175 | current_root = root 176 | tokens = get_tokens(es, slugline, '/') 177 | for index in range(len(tokens)-1): 178 | pair = tokens[index : index+2] 179 | parent = current_root.find(pair[0]) 180 | if parent is None: 181 | parent = Node(pair[0], delimiter='/') 182 | current_root.add_child(parent) 183 | current_root = parent 184 | parent.add_child(Node(pair[1], delimiter='/')) 185 | 186 | print(corpora[i][1], 'index') 187 | index_sections(es, root, index_name=corpora[i][1]) -------------------------------------------------------------------------------- /documents-api/topics.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | from elasticsearch import Elasticsearch 3 | 4 | 5 | def get_topics(dir, lang='en'): 6 | topics = {} 7 | for filename in os.listdir(dir): 8 | file = open(dir+filename, "r") 9 | json_article = json.load(file) 10 | 11 | for topic in json_article['topics']: 12 | id_parts = topic['id'].split(':') 13 | topics[topic['id']] = { 14 | 'id': topic['id'], 15 | 'name': topic['name'], 16 | 'parent': topic['parent'], 17 | 'url': 'http://cv.iptc.org/newscodes/mediatopic/'+id_parts[1]+'?lang='+lang, 18 | 'search': topic['name']+' ('+id_parts[1]+')' 19 | } 20 | return topics 21 | 22 | 23 | def index_topics(es, index_name, doc_type, topics={}, update=True): 24 | properties = { 25 | 'properties': { 26 | 'id': {'type': 'keyword'}, 27 | 'name': {'type': 'keyword'}, 28 | 'parent': {'type': 'keyword'}, 29 | 'url': {'type': 'keyword'}, 30 | 'search': {'type': 'keyword'} 31 | } 32 | } 33 | request_body = { 34 | 'mappings': { 35 | doc_type: properties 36 | } 37 | } 38 | 39 | if update: 40 | es.indices.put_mapping(index=index_name, doc_type=doc_type, body=properties) 41 | else: 42 | es.indices.create(index=index_name, body=request_body) 43 | for id in topics: 44 | topic = topics[id] 45 | es.index(index=index_name, doc_type=doc_type, id=id, body=topic) 46 | 47 | 48 | if __name__ == '__main__': 49 | corpora = [ 50 | ['/disk1_data/EXTRA/APA dataset/json_v2/', 'de', 'apa'], 51 | ['/disk1_data/EXTRA/Reuters dataset/json_v2/', 'en', 'reuters'] 52 | ] 53 | 54 | es = Elasticsearch() 55 | for i in 0, 1: 56 | topics = get_topics(corpora[i][0], corpora[i][1]) 57 | print(len(topics), 'topics found for', corpora[i][2]) 58 | index_topics(es, corpora[i][2], 'topics', topics) -------------------------------------------------------------------------------- /extra_platform_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/extra_platform_arch.png -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | MAINTAINER Brendan Quinn 4 | 5 | RUN apt-get update 6 | RUN apt-get -y upgrade 7 | 8 | RUN DEBIAN_FRONTEND=noninteractive apt-get -y install apache2 9 | 10 | # Enable apache mods. 11 | RUN a2enmod rewrite 12 | RUN a2enmod headers 13 | 14 | # Manually set up the apache environment variables 15 | ENV APACHE_RUN_USER www-data 16 | ENV APACHE_RUN_GROUP www-data 17 | ENV APACHE_LOG_DIR /var/log/apache2 18 | ENV APACHE_LOCK_DIR /var/lock/apache2 19 | ENV APACHE_PID_FILE /var/run/apache2.pid 20 | 21 | # Copy html into place. 22 | ADD html /var/www/site 23 | 24 | # Update the default apache site with the config we created. 25 | ADD apache-config.conf /etc/apache2/sites-enabled/000-default.conf 26 | 27 | EXPOSE 80 28 | 29 | # By default, simply start apache. 30 | CMD /usr/sbin/apache2ctl -D FOREGROUND 31 | -------------------------------------------------------------------------------- /ui/apache-config.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerAdmin me@mydomain.com 3 | DocumentRoot /var/www/site 4 | 5 | 6 | Options Indexes FollowSymLinks MultiViews 7 | AllowOverride All 8 | Order deny,allow 9 | Allow from all 10 | 11 | 12 | ErrorLog ${APACHE_LOG_DIR}/error.log 13 | CustomLog ${APACHE_LOG_DIR}/access.log combined 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/html/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule ^$ /taxonomies [L] 3 | 4 | AuthType Basic 5 | AuthName "Password Protected Area" 6 | AuthUserFile /var/www/site/.htpasswd 7 | Require valid-user 8 | -------------------------------------------------------------------------------- /ui/html/.htpasswd: -------------------------------------------------------------------------------- 1 | extra:$apr1$LevY1afU$xW9HEvRd3I6tI24kffVC70 2 | -------------------------------------------------------------------------------- /ui/html/documents/css/jquery.comiseo.daterangepicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017 Tamble, Inc. 3 | * Licensed under MIT (https://github.com/tamble/jquery-ui-daterangepicker/raw/master/LICENSE.txt) 4 | */ 5 | 6 | .comiseo-daterangepicker-triggerbutton.ui-button { 7 | text-align: left; 8 | width: 200px; 9 | font-size: 13px; 10 | margin: 10px; 11 | } 12 | 13 | .comiseo-daterangepicker-triggerbutton .ui-button-icon { /* fix v1.12 */ 14 | position: absolute; 15 | right: 0; 16 | top: 50%; 17 | margin-top: -8px; 18 | } 19 | 20 | .comiseo-daterangepicker { 21 | position: absolute; 22 | padding: 5px; 23 | } 24 | 25 | .comiseo-daterangepicker-mask { 26 | margin: 0; 27 | padding: 0; 28 | position: fixed; 29 | left: 0; 30 | top: 0; 31 | height: 100%; 32 | width: 100%; 33 | /* required for IE */ 34 | background-color: #fff; 35 | opacity: 0; 36 | filter: alpha(opacity = 0); 37 | } 38 | 39 | .comiseo-daterangepicker-presets, 40 | .comiseo-daterangepicker-calendar { 41 | display: table-cell; 42 | vertical-align: top; 43 | height: 230px; 44 | } 45 | 46 | .comiseo-daterangepicker-right .comiseo-daterangepicker-presets { 47 | padding: 2px 7px 7px 2px; 48 | display: none; 49 | } 50 | 51 | .comiseo-daterangepicker-left .comiseo-daterangepicker-presets { 52 | padding: 2px 2px 7px 7px; 53 | } 54 | 55 | .comiseo-daterangepicker-presets .ui-menu { 56 | padding: 2px; /* fix v1.11 */ 57 | white-space: nowrap; 58 | } 59 | 60 | .comiseo-daterangepicker-presets .ui-menu-item { /* fix v1.11 */ 61 | padding: 0; 62 | } 63 | 64 | .comiseo-daterangepicker-presets .ui-menu-item > * { /* fix v1.11 */ 65 | text-decoration: none; 66 | display: block; 67 | padding: 2px 0.4em; 68 | line-height: 1.5; 69 | min-height: 0; /* support: IE7 */ 70 | } 71 | 72 | .comiseo-daterangepicker .ui-widget-content, 73 | .comiseo-daterangepicker .ui-datepicker .ui-state-highlight { 74 | border-width: 0; 75 | } 76 | 77 | .comiseo-daterangepicker > .comiseo-daterangepicker-main.ui-widget-content { 78 | border-bottom-width: 1px; 79 | } 80 | 81 | .comiseo-daterangepicker .ui-datepicker .ui-datepicker-today .ui-state-highlight { 82 | border-width: 1px; 83 | } 84 | 85 | .comiseo-daterangepicker-right .comiseo-daterangepicker-calendar { 86 | border-left-width: 1px; 87 | padding-left: 5px; 88 | } 89 | 90 | .comiseo-daterangepicker-left .comiseo-daterangepicker-calendar { 91 | border-right-width: 1px; 92 | padding-right: 5px; 93 | } 94 | 95 | .comiseo-daterangepicker-right .comiseo-daterangepicker-buttonpanel { 96 | float: left; 97 | } 98 | 99 | .comiseo-daterangepicker-left .comiseo-daterangepicker-buttonpanel { 100 | float: right; 101 | } 102 | 103 | .comiseo-daterangepicker-buttonpanel > button { 104 | margin-top: 6px; 105 | } 106 | 107 | .comiseo-daterangepicker-right .comiseo-daterangepicker-buttonpanel > button { 108 | margin-right: 6px; 109 | } 110 | 111 | .comiseo-daterangepicker-left .comiseo-daterangepicker-buttonpanel > button { 112 | margin-left: 6px; 113 | } 114 | 115 | /* themeable styles */ 116 | .comiseo-daterangepicker-calendar .ui-state-highlight a.ui-state-default { 117 | background: #b0c4de; 118 | color: #fff; 119 | } -------------------------------------------------------------------------------- /ui/html/documents/imgs/corpus-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/corpus-16.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/date-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/date-16.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/delete.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/link-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/link-16.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/loading.gif -------------------------------------------------------------------------------- /ui/html/documents/imgs/search-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/search-12.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/sections-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/sections-16.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/split-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/split-16.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/topics-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/topics-16.png -------------------------------------------------------------------------------- /ui/html/documents/imgs/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/documents/imgs/undo.png -------------------------------------------------------------------------------- /ui/html/documents/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IPTC 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 39 |
40 |
41 |
42 |
Loading sluglines, please wait..... 43 | loading.. 44 |
45 |
Zero sluglines
46 |
47 | 55 |
    56 |
    57 |
      58 |
      59 | × 60 |
      61 |
      62 |

      EXTRA

      63 | 64 | 67 | 71 | 74 |
      75 | 122 |
      SEARCH 123 |
      124 |
      125 |
      126 |
      127 |
      Loading documents, please wait..... 128 | loading.. 129 |
      130 |
      Zero documents
      131 |
      132 |
        133 |
        134 |
        135 |
          136 |
          137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /ui/html/documents/js/ajax_abort.js: -------------------------------------------------------------------------------- 1 | var xhrPool = []; 2 | $(document).ajaxSend(function (e, jqXHR, options) { 3 | if (options.url.indexOf('/articles/') > -1) { 4 | xhrPool.push(jqXHR); 5 | } 6 | }); 7 | $(document).ajaxComplete(function (e, jqXHR, options) { 8 | if (options.url.indexOf('/articles/') > -1) { 9 | xhrPool = $.grep(xhrPool, function (x) { 10 | return x != jqXHR 11 | }); 12 | } 13 | }); 14 | var abort = function () { 15 | $.each(xhrPool, function (idx, jqXHR) { 16 | jqXHR.abort(); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /ui/html/documents/js/jquery.reveal.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | 4 | $.fn.reveal = function () { 5 | 6 | 7 | var defaults = { 8 | animation: 'fadeAndPop', 9 | animationspeed: 300, 10 | closeonbackgroundclick: true, 11 | dismissmodalclass: 'close-reveal-modal' 12 | }; 13 | 14 | var options = $.extend({}, defaults, options); 15 | 16 | return this.each(function () { 17 | 18 | 19 | var modal = $(this), 20 | topMeasure = parseInt(modal.css('top')), 21 | topOffset = modal.height() + topMeasure, 22 | locked = false, 23 | modalBG = $('.reveal-modal-bg'); 24 | 25 | 26 | if (modalBG.length == 0) { 27 | modalBG = $('
          ').insertAfter(modal); 28 | } 29 | 30 | 31 | modal.bind('reveal:open', function () { 32 | modalBG.unbind('click.modalEvent'); 33 | $('.' + options.dismissmodalclass).unbind('click.modalEvent'); 34 | if (!locked) { 35 | lockModal(); 36 | modal.css({ 37 | 'top': 50,//$(document).scrollTop() - topOffset, 38 | 'opacity': 0, 39 | 'visibility': 'visible' 40 | }); 41 | modalBG.fadeIn(options.animationspeed / 2); 42 | modal.delay(options.animationspeed / 2).animate({ 43 | "top": '50px',//$(document).scrollTop() + topMeasure + 'px', 44 | "opacity": 1 45 | }, options.animationspeed, unlockModal()); 46 | } 47 | modal.unbind('reveal:open'); 48 | }); 49 | 50 | 51 | modal.bind('reveal:close', function () { 52 | 53 | if (!locked) { 54 | lockModal(); 55 | modalBG.delay(options.animationspeed).fadeOut(options.animationspeed); 56 | modal.animate({ 57 | "top": 50,// $(document).scrollTop() - topOffset + 'px', 58 | "opacity": 0 59 | }, options.animationspeed / 2, function () { 60 | modal.css({ 61 | 'top': 50,//topMeasure, 62 | 'opacity': 1, 63 | 'visibility': 'hidden' 64 | }); 65 | unlockModal(); 66 | }); 67 | } 68 | modal.unbind('reveal:close'); 69 | if ($(this).attr('id') === "myModal") { 70 | var id = $('#myModal').find('.media_topic').find('img').eq(0).attr('data-parent_id'); 71 | $('li[data-id="' + id + '"]').find('.media_topic').eq(0).html($('#myModal').find('.media_topic').eq(0).html()); 72 | $('li[data-id="' + id + '"]').find('.media_topic').eq(1).html($('#myModal').find('.media_topic').eq(1).html()); 73 | $('li[data-id="' + id + '"]').find('.media_topic').eq(2).html($('#myModal').find('.media_topic').eq(2).html()); 74 | } 75 | }); 76 | 77 | 78 | modal.trigger('reveal:open') 79 | 80 | 81 | var closeButton = $('.' + options.dismissmodalclass).bind('click.modalEvent', function () { 82 | modal.trigger('reveal:close') 83 | }); 84 | 85 | if (options.closeonbackgroundclick) { 86 | modalBG.bind('click.modalEvent', function () { 87 | modal.trigger('reveal:close') 88 | }); 89 | } 90 | $('body').keyup(function (e) { 91 | if (e.which === 27) { 92 | modal.trigger('reveal:close'); 93 | } 94 | }); 95 | 96 | function unlockModal() { 97 | locked = false; 98 | } 99 | 100 | function lockModal() { 101 | locked = true; 102 | } 103 | 104 | }); 105 | } 106 | })(jQuery); -------------------------------------------------------------------------------- /ui/html/documents/js/jquery.twbsPagination.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Bootstrap Pagination v1.4.1 3 | * https://github.com/esimakin/twbs-pagination 4 | * 5 | * Copyright 2014-2016, Eugene Simakin 6 | * Released under Apache-2.0 license 7 | * http://apache.org/licenses/LICENSE-2.0.html 8 | */ 9 | !function(a,b,c,d){"use strict";var e=a.fn.twbsPagination,f=function(b,c){if(this.$element=a(b),this.options=a.extend({},a.fn.twbsPagination.defaults,c),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw new Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw new Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw new Error("Visible pages option is not correct!");if(this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.$element.trigger("page",1),this;this.options.totalPages"),this.$listContainer.addClass(this.options.paginationClass),"UL"!==d&&this.$element.append(this.$listContainer),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.currentPage=this.options.startPage,this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};f.prototype={constructor:f,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(a){if(a<1||a>this.options.totalPages)throw new Error("Page is incorrect.");return this.currentPage=a,this.render(this.getPages(a)),this.setupEvents(),this.$element.trigger("page",a),this},enable:function(){this.show(this.currentPage)},disable:function(){var b=this;this.$listContainer.off("click").on("click","li",function(a){a.preventDefault()}),this.$listContainer.children().each(function(){var c=a(this);c.hasClass(b.options.activeClass)||a(this).addClass(b.options.disabledClass)})},buildListItems:function(a){var b=[];if(this.options.first&&b.push(this.buildItem("first",1)),this.options.prev){var c=a.currentPage>1?a.currentPage-1:this.options.loop?this.options.totalPages:1;b.push(this.buildItem("prev",c))}for(var d=0;d"),e=a(""),f=this.options[b]?this.makeText(this.options[b],c):c;return d.addClass(this.options[b+"Class"]),d.data("page",c),d.data("page-type",b),d.append(e.attr("href",this.makeHref(c)).addClass(this.options.anchorClass).html(f)),d},getPages:function(a){var b=[],c=Math.floor(this.options.visiblePages/2),d=a-c+1-this.options.visiblePages%2,e=a+c;d<=0&&(d=1,e=this.options.visiblePages),e>this.options.totalPages&&(d=this.options.totalPages-this.options.visiblePages+1,e=this.options.totalPages);for(var f=d;f<=e;)b.push(f),f++;return{currentPage:a,numeric:b}},render:function(b){var c=this;this.$listContainer.children().remove();var d=this.buildListItems(b);a.each(d,function(a,b){c.$listContainer.append(b)}),this.$listContainer.children().each(function(){var d=a(this),e=d.data("page-type");switch(e){case"page":d.data("page")===b.currentPage&&d.addClass(c.options.activeClass);break;case"first":d.toggleClass(c.options.disabledClass,1===b.currentPage);break;case"last":d.toggleClass(c.options.disabledClass,b.currentPage===c.options.totalPages);break;case"prev":d.toggleClass(c.options.disabledClass,!c.options.loop&&1===b.currentPage);break;case"next":d.toggleClass(c.options.disabledClass,!c.options.loop&&b.currentPage===c.options.totalPages)}})},setupEvents:function(){var b=this;this.$listContainer.off("click").on("click","li",function(c){var d=a(this);return!d.hasClass(b.options.disabledClass)&&!d.hasClass(b.options.activeClass)&&(!b.options.href&&c.preventDefault(),void b.show(parseInt(d.data("page"))))})},makeHref:function(a){return this.options.href?this.generateQueryString(a):"#"},makeText:function(a,b){return a.replace(this.options.pageVariable,b).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(a){var b=this.getSearchString(a),c=new RegExp(this.options.pageVariable+"(=([^&#]*)|&|#|$)"),d=c.exec(b);return d&&d[2]?(d=decodeURIComponent(d[2]),d=parseInt(d),isNaN(d)?null:d):null},generateQueryString:function(a,b){var c=this.getSearchString(b),d=new RegExp(this.options.pageVariable+"=*[^&#]*");return c?"?"+c.replace(d,this.options.pageVariable+"="+a):""},getSearchString:function(a){var c=a||b.location.search;return""===c?null:(0===c.indexOf("?")&&(c=c.substr(1)),c)},getCurrentPage:function(){return this.currentPage}},a.fn.twbsPagination=function(b){var c,e=Array.prototype.slice.call(arguments,1),g=a(this),h=g.data("twbs-pagination"),i="object"==typeof b?b:{};return h||g.data("twbs-pagination",h=new f(this,i)),"string"==typeof b&&(c=h[b].apply(h,e)),c===d?g:c},a.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},a.fn.twbsPagination.Constructor=f,a.fn.twbsPagination.noConflict=function(){return a.fn.twbsPagination=e,this},a.fn.twbsPagination.version="1.4.1"}(window.jQuery,window,document); -------------------------------------------------------------------------------- /ui/html/documents/js/xml.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2007 Lev Muchnik . All rights reserved. 2 | * You may copy and modify this script as long as the above copyright notice, 3 | * this condition and the following disclaimer is left intact. 4 | * This software is provided by the author "AS IS" and no warranties are 5 | * implied, including fitness for a particular purpose. In no event shall 6 | * the author be liable for any damages arising in any way out of the use 7 | * of this software, even if advised of the possibility of such damage. 8 | * $Date: 2007-10-03 19:08:15 -0700 (Wed, 03 Oct 2007) $ 9 | */ 10 | 11 | 12 | function LoadXMLDom(ParentElementID, xmlDoc) { 13 | if (xmlDoc) { 14 | var xmlHolderElement = GetParentElement(ParentElementID); 15 | if (xmlHolderElement == null) { 16 | return false; 17 | } 18 | while (xmlHolderElement.childNodes.length) { 19 | xmlHolderElement.removeChild(xmlHolderElement.childNodes.item(xmlHolderElement.childNodes.length - 1)); 20 | } 21 | var Result = ShowXML(xmlHolderElement, xmlDoc.documentElement, 0); 22 | return Result; 23 | } 24 | else { 25 | return false; 26 | } 27 | } 28 | function LoadXMLString(ParentElementID, XMLString) { 29 | xmlDoc = CreateXMLDOM(XMLString); 30 | return LoadXMLDom(ParentElementID, xmlDoc); 31 | } 32 | //////////////////////////////////////////////////////////// 33 | // HELPER FUNCTIONS - SHOULD NOT BE DIRECTLY CALLED BY USERS 34 | //////////////////////////////////////////////////////////// 35 | function GetParentElement(ParentElementID) { 36 | if (typeof(ParentElementID) == 'string') { 37 | return document.getElementById(ParentElementID); 38 | } 39 | else if (typeof(ParentElementID) == 'object') { 40 | return ParentElementID; 41 | } 42 | else { 43 | return null; 44 | } 45 | } 46 | function CreateXMLDOM(XMLStr) { 47 | if (window.ActiveXObject) { 48 | xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); 49 | xmlDoc.loadXML(XMLStr); 50 | return xmlDoc; 51 | } 52 | else if (document.implementation && document.implementation.createDocument) { 53 | var parser = new DOMParser(); 54 | return parser.parseFromString(XMLStr, "text/xml"); 55 | } 56 | else { 57 | return null; 58 | } 59 | } 60 | 61 | var IDCounter = 0; 62 | var NestingIndent = 15; 63 | function ShowXML(xmlHolderElement, RootNode, indent) { 64 | if (RootNode == null || xmlHolderElement == null) { 65 | return false; 66 | } 67 | var Result = true; 68 | var TagEmptyElement = document.createElement('div'); 69 | TagEmptyElement.className = 'Element'; 70 | TagEmptyElement.style.position = 'relative'; 71 | TagEmptyElement.style.left = NestingIndent + 'px'; 72 | IDCounter++; 73 | if (RootNode.childNodes.length == 0) { 74 | var ClickableElement = AddTextNode(TagEmptyElement, '', 'Clickable'); 75 | ClickableElement.id = 'div_empty_' + IDCounter; 76 | AddTextNode(TagEmptyElement, '<', 'Utility'); 77 | AddTextNode(TagEmptyElement, RootNode.nodeName, 'NodeName') 78 | for (var i = 0; RootNode.attributes && i < RootNode.attributes.length; ++i) { 79 | CurrentAttribute = RootNode.attributes.item(i); 80 | AddTextNode(TagEmptyElement, ' ' + CurrentAttribute.nodeName, 'AttributeName'); 81 | AddTextNode(TagEmptyElement, '=', 'Utility'); 82 | AddTextNode(TagEmptyElement, '"' + CurrentAttribute.nodeValue + '"', 'AttributeValue'); 83 | } 84 | AddTextNode(TagEmptyElement, ' />'); 85 | xmlHolderElement.appendChild(TagEmptyElement); 86 | //SetVisibility(TagEmptyElement,true); 87 | } 88 | else { // mo child nodes 89 | 90 | 91 | var ClickableElement = AddTextNode(TagEmptyElement, '+', 'Clickable'); 92 | ClickableElement.onclick = function () { 93 | ToggleElementVisibility(this); 94 | }; 95 | ClickableElement.id = 'div_empty_' + IDCounter; 96 | 97 | AddTextNode(TagEmptyElement, '<', 'Utility'); 98 | AddTextNode(TagEmptyElement, RootNode.nodeName, 'NodeName') 99 | for (var i = 0; RootNode.attributes && i < RootNode.attributes.length; ++i) { 100 | CurrentAttribute = RootNode.attributes.item(i); 101 | AddTextNode(TagEmptyElement, ' ' + CurrentAttribute.nodeName, 'AttributeName'); 102 | AddTextNode(TagEmptyElement, '=', 'Utility'); 103 | AddTextNode(TagEmptyElement, '"' + CurrentAttribute.nodeValue + '"', 'AttributeValue'); 104 | } 105 | 106 | AddTextNode(TagEmptyElement, '> ', 'Utility'); 109 | xmlHolderElement.appendChild(TagEmptyElement); 110 | SetVisibility(TagEmptyElement, false); 111 | //---------------------------------------------- 112 | 113 | var TagElement = document.createElement('div'); 114 | TagElement.className = 'Element'; 115 | TagElement.style.position = 'relative'; 116 | TagElement.style.left = NestingIndent + 'px'; 117 | ClickableElement = AddTextNode(TagElement, '-', 'Clickable'); 118 | ClickableElement.onclick = function () { 119 | ToggleElementVisibility(this); 120 | }; 121 | ClickableElement.id = 'div_content_' + IDCounter; 122 | AddTextNode(TagElement, '<', 'Utility'); 123 | AddTextNode(TagElement, RootNode.nodeName, 'NodeName'); 124 | 125 | for (var i = 0; RootNode.attributes && i < RootNode.attributes.length; ++i) { 126 | CurrentAttribute = RootNode.attributes.item(i); 127 | AddTextNode(TagElement, ' ' + CurrentAttribute.nodeName, 'AttributeName'); 128 | AddTextNode(TagElement, '=', 'Utility'); 129 | AddTextNode(TagElement, '"' + CurrentAttribute.nodeValue + '"', 'AttributeValue'); 130 | } 131 | AddTextNode(TagElement, '>', 'Utility'); 132 | TagElement.appendChild(document.createElement('br')); 133 | var NodeContent = null; 134 | for (var i = 0; RootNode.childNodes && i < RootNode.childNodes.length; ++i) { 135 | if (RootNode.childNodes.item(i).nodeName != '#text') { 136 | Result &= ShowXML(TagElement, RootNode.childNodes.item(i), indent + 1); 137 | } 138 | else { 139 | NodeContent = RootNode.childNodes.item(i).nodeValue; 140 | } 141 | } 142 | if (RootNode.nodeValue) { 143 | NodeContent = RootNode.nodeValue; 144 | } 145 | if (NodeContent) { 146 | var ContentElement = document.createElement('div'); 147 | ContentElement.style.position = 'relative'; 148 | ContentElement.style.left = NestingIndent + 'px'; 149 | AddTextNode(ContentElement, NodeContent, 'NodeValue'); 150 | TagElement.appendChild(ContentElement); 151 | } 152 | AddTextNode(TagElement, ' ', 'Utility'); 155 | xmlHolderElement.appendChild(TagElement); 156 | } 157 | 158 | // if (indent==0) { ToggleElementVisibility(TagElement.childNodes(0)); } - uncomment to collapse the external element 159 | return Result; 160 | } 161 | function AddTextNode(ParentNode, Text, Class) { 162 | NewNode = document.createElement('span'); 163 | if (Class) { 164 | NewNode.className = Class; 165 | } 166 | if (Text) { 167 | NewNode.appendChild(document.createTextNode(Text)); 168 | } 169 | if (ParentNode) { 170 | ParentNode.appendChild(NewNode); 171 | } 172 | return NewNode; 173 | } 174 | function CompatibleGetElementByID(id) { 175 | if (!id) { 176 | return null; 177 | } 178 | if (document.getElementById) { // DOM3 = IE5, NS6 179 | return document.getElementById(id); 180 | } 181 | else { 182 | if (document.layers) { // Netscape 4 183 | return document.id; 184 | } 185 | else { // IE 4 186 | return document.all.id; 187 | } 188 | } 189 | } 190 | function SetVisibility(HTMLElement, Visible) { 191 | if (!HTMLElement) { 192 | return; 193 | } 194 | var VisibilityStr = (Visible) ? 'block' : 'none'; 195 | if (document.getElementById) { // DOM3 = IE5, NS6 196 | HTMLElement.style.display = VisibilityStr; 197 | } 198 | else { 199 | if (document.layers) { // Netscape 4 200 | HTMLElement.display = VisibilityStr; 201 | } 202 | else { // IE 4 203 | HTMLElement.id.style.display = VisibilityStr; 204 | } 205 | } 206 | } 207 | function ToggleElementVisibility(Element) { 208 | if (!Element || !Element.id) { 209 | return; 210 | } 211 | try { 212 | ElementType = Element.id.slice(0, Element.id.lastIndexOf('_') + 1); 213 | ElementID = parseInt(Element.id.slice(Element.id.lastIndexOf('_') + 1)); 214 | } 215 | catch (e) { 216 | return; 217 | } 218 | var ElementToHide = null; 219 | var ElementToShow = null; 220 | if (ElementType == 'div_content_') { 221 | ElementToHide = 'div_content_' + ElementID; 222 | ElementToShow = 'div_empty_' + ElementID; 223 | } 224 | else if (ElementType == 'div_empty_') { 225 | ElementToShow = 'div_content_' + ElementID; 226 | ElementToHide = 'div_empty_' + ElementID; 227 | } 228 | ElementToHide = CompatibleGetElementByID(ElementToHide); 229 | ElementToShow = CompatibleGetElementByID(ElementToShow); 230 | if (ElementToHide) { 231 | ElementToHide = ElementToHide.parentNode; 232 | } 233 | if (ElementToShow) { 234 | ElementToShow = ElementToShow.parentNode; 235 | } 236 | SetVisibility(ElementToHide, false); 237 | SetVisibility(ElementToShow, true); 238 | } -------------------------------------------------------------------------------- /ui/html/editor/imgs/add-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/editor/imgs/add-icon.png -------------------------------------------------------------------------------- /ui/html/editor/imgs/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/editor/imgs/back.png -------------------------------------------------------------------------------- /ui/html/editor/imgs/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/editor/imgs/delete.png -------------------------------------------------------------------------------- /ui/html/editor/imgs/search-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/editor/imgs/search-12.png -------------------------------------------------------------------------------- /ui/html/editor/js/jquery.reveal.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | 4 | $.fn.reveal = function () { 5 | 6 | 7 | var defaults = { 8 | animation: 'fadeAndPop', 9 | animationspeed: 300, 10 | closeonbackgroundclick: false, 11 | dismissmodalclass: 'close-reveal-modal' 12 | }; 13 | 14 | var options = $.extend({}, defaults, options); 15 | 16 | return this.each(function () { 17 | 18 | 19 | var modal = $(this), 20 | topMeasure = parseInt(modal.css('top')), 21 | topOffset = modal.height() + topMeasure, 22 | locked = false, 23 | modalBG = $('.reveal-modal-bg'); 24 | 25 | 26 | if (modalBG.length == 0) { 27 | modalBG = $('
          ').insertAfter(modal); 28 | } 29 | 30 | 31 | modal.bind('reveal:open', function () { 32 | modalBG.unbind('click.modalEvent'); 33 | $('.' + options.dismissmodalclass).unbind('click.modalEvent'); 34 | if (!locked) { 35 | lockModal(); 36 | modal.css({ 37 | 'top': 50,//$(document).scrollTop() - topOffset, 38 | 'opacity': 0, 39 | 'visibility': 'visible' 40 | }); 41 | modalBG.fadeIn(options.animationspeed / 2); 42 | modal.delay(options.animationspeed / 2).animate({ 43 | "top": '50px',//$(document).scrollTop() + topMeasure + 'px', 44 | "opacity": 1 45 | }, options.animationspeed, unlockModal()); 46 | } 47 | modal.unbind('reveal:open'); 48 | }); 49 | 50 | 51 | modal.bind('reveal:close', function () { 52 | 53 | if (!locked) { 54 | lockModal(); 55 | modalBG.delay(options.animationspeed).fadeOut(options.animationspeed); 56 | modal.animate({ 57 | "top": 50,// $(document).scrollTop() - topOffset + 'px', 58 | "opacity": 0 59 | }, options.animationspeed / 2, function () { 60 | modal.css({ 61 | 'top': 50,//topMeasure, 62 | 'opacity': 1, 63 | 'visibility': 'hidden' 64 | }); 65 | unlockModal(); 66 | }); 67 | } 68 | modal.unbind('reveal:close'); 69 | if ($(this).attr('id') === "myModal") { 70 | var id = $('#myModal').find('.media_topic').find('img').eq(0).attr('data-parent_id'); 71 | $('li[data-id="' + id + '"]').find('.media_topic').eq(0).html($('#myModal').find('.media_topic').eq(0).html()); 72 | $('li[data-id="' + id + '"]').find('.media_topic').eq(1).html($('#myModal').find('.media_topic').eq(1).html()); 73 | } 74 | }); 75 | 76 | 77 | modal.trigger('reveal:open') 78 | 79 | 80 | var closeButton = $('.' + options.dismissmodalclass).bind('click.modalEvent', function () { 81 | modal.trigger('reveal:close') 82 | }); 83 | 84 | if (options.closeonbackgroundclick) { 85 | modalBG.bind('click.modalEvent', function () { 86 | modal.trigger('reveal:close') 87 | }); 88 | } 89 | $('body').keyup(function (e) { 90 | if (e.which === 27) { 91 | modal.trigger('reveal:close'); 92 | } 93 | }); 94 | 95 | function unlockModal() { 96 | locked = false; 97 | } 98 | 99 | function lockModal() { 100 | locked = true; 101 | } 102 | 103 | }); 104 | } 105 | })(jQuery); -------------------------------------------------------------------------------- /ui/html/editor/js/jquery.twbsPagination.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Bootstrap Pagination v1.4.1 3 | * https://github.com/esimakin/twbs-pagination 4 | * 5 | * Copyright 2014-2016, Eugene Simakin 6 | * Released under Apache-2.0 license 7 | * http://apache.org/licenses/LICENSE-2.0.html 8 | */ 9 | !function(a,b,c,d){"use strict";var e=a.fn.twbsPagination,f=function(b,c){if(this.$element=a(b),this.options=a.extend({},a.fn.twbsPagination.defaults,c),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw new Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw new Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw new Error("Visible pages option is not correct!");if(this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.$element.trigger("page",1),this;this.options.totalPages"),this.$listContainer.addClass(this.options.paginationClass),"UL"!==d&&this.$element.append(this.$listContainer),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.currentPage=this.options.startPage,this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};f.prototype={constructor:f,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(a){if(a<1||a>this.options.totalPages)throw new Error("Page is incorrect.");return this.currentPage=a,this.render(this.getPages(a)),this.setupEvents(),this.$element.trigger("page",a),this},enable:function(){this.show(this.currentPage)},disable:function(){var b=this;this.$listContainer.off("click").on("click","li",function(a){a.preventDefault()}),this.$listContainer.children().each(function(){var c=a(this);c.hasClass(b.options.activeClass)||a(this).addClass(b.options.disabledClass)})},buildListItems:function(a){var b=[];if(this.options.first&&b.push(this.buildItem("first",1)),this.options.prev){var c=a.currentPage>1?a.currentPage-1:this.options.loop?this.options.totalPages:1;b.push(this.buildItem("prev",c))}for(var d=0;d"),e=a(""),f=this.options[b]?this.makeText(this.options[b],c):c;return d.addClass(this.options[b+"Class"]),d.data("page",c),d.data("page-type",b),d.append(e.attr("href",this.makeHref(c)).addClass(this.options.anchorClass).html(f)),d},getPages:function(a){var b=[],c=Math.floor(this.options.visiblePages/2),d=a-c+1-this.options.visiblePages%2,e=a+c;d<=0&&(d=1,e=this.options.visiblePages),e>this.options.totalPages&&(d=this.options.totalPages-this.options.visiblePages+1,e=this.options.totalPages);for(var f=d;f<=e;)b.push(f),f++;return{currentPage:a,numeric:b}},render:function(b){var c=this;this.$listContainer.children().remove();var d=this.buildListItems(b);a.each(d,function(a,b){c.$listContainer.append(b)}),this.$listContainer.children().each(function(){var d=a(this),e=d.data("page-type");switch(e){case"page":d.data("page")===b.currentPage&&d.addClass(c.options.activeClass);break;case"first":d.toggleClass(c.options.disabledClass,1===b.currentPage);break;case"last":d.toggleClass(c.options.disabledClass,b.currentPage===c.options.totalPages);break;case"prev":d.toggleClass(c.options.disabledClass,!c.options.loop&&1===b.currentPage);break;case"next":d.toggleClass(c.options.disabledClass,!c.options.loop&&b.currentPage===c.options.totalPages)}})},setupEvents:function(){var b=this;this.$listContainer.off("click").on("click","li",function(c){var d=a(this);return!d.hasClass(b.options.disabledClass)&&!d.hasClass(b.options.activeClass)&&(!b.options.href&&c.preventDefault(),void b.show(parseInt(d.data("page"))))})},makeHref:function(a){return this.options.href?this.generateQueryString(a):"#"},makeText:function(a,b){return a.replace(this.options.pageVariable,b).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(a){var b=this.getSearchString(a),c=new RegExp(this.options.pageVariable+"(=([^&#]*)|&|#|$)"),d=c.exec(b);return d&&d[2]?(d=decodeURIComponent(d[2]),d=parseInt(d),isNaN(d)?null:d):null},generateQueryString:function(a,b){var c=this.getSearchString(b),d=new RegExp(this.options.pageVariable+"=*[^&#]*");return c?"?"+c.replace(d,this.options.pageVariable+"="+a):""},getSearchString:function(a){var c=a||b.location.search;return""===c?null:(0===c.indexOf("?")&&(c=c.substr(1)),c)},getCurrentPage:function(){return this.currentPage}},a.fn.twbsPagination=function(b){var c,e=Array.prototype.slice.call(arguments,1),g=a(this),h=g.data("twbs-pagination"),i="object"==typeof b?b:{};return h||g.data("twbs-pagination",h=new f(this,i)),"string"==typeof b&&(c=h[b].apply(h,e)),c===d?g:c},a.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},a.fn.twbsPagination.Constructor=f,a.fn.twbsPagination.noConflict=function(){return a.fn.twbsPagination=e,this},a.fn.twbsPagination.version="1.4.1"}(window.jQuery,window,document); -------------------------------------------------------------------------------- /ui/html/editor/js/jquery.wookmark.js: -------------------------------------------------------------------------------- 1 | (function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function i(t){n(function(){var i,e;for(i=0;t.length>i;i++)e=t[i],e.obj.css(e.css)})}function e(i){return t.trim(i).toLowerCase()}var s,h,o;o=function(t,i){return function(){return t.apply(i,arguments)}},h={align:"center",autoResize:!1,comparator:null,container:t("body"),direction:void 0,ignoreInactiveItems:!0,itemWidth:0,fillEmptySpace:!1,flexibleWidth:0,offset:2,outerOffset:0,onLayoutChanged:void 0,possibleFilters:[],resizeDelay:50,verticalOffset:void 0};var n=window.requestAnimationFrame||function(t){t()},r=t(window);s=function(){function s(i,e){this.handler=i,this.columns=this.containerWidth=this.resizeTimer=null,this.activeItemCount=0,this.itemHeightsDirty=!0,this.placeholders=[],t.extend(!0,this,h,e),this.verticalOffset=this.verticalOffset||this.offset,this.update=o(this.update,this),this.onResize=o(this.onResize,this),this.onRefresh=o(this.onRefresh,this),this.getItemWidth=o(this.getItemWidth,this),this.layout=o(this.layout,this),this.layoutFull=o(this.layoutFull,this),this.layoutColumns=o(this.layoutColumns,this),this.filter=o(this.filter,this),this.clear=o(this.clear,this),this.getActiveItems=o(this.getActiveItems,this),this.refreshPlaceholders=o(this.refreshPlaceholders,this),this.sortElements=o(this.sortElements,this),this.updateFilterClasses=o(this.updateFilterClasses,this),this.updateFilterClasses(),this.autoResize&&r.bind("resize.wookmark",this.onResize),this.container.bind("refreshWookmark",this.onRefresh)}return s.prototype.updateFilterClasses=function(){for(var t,i,s,h,o=0,n=0,r=0,a={},l=this.possibleFilters;this.handler.length>o;o++)if(i=this.handler.eq(o),t=i.data("filterClass"),"object"==typeof t&&t.length>0)for(n=0;t.length>n;n++)s=e(t[n]),a[s]===void 0&&(a[s]=[]),a[s].push(i[0]);for(;l.length>r;r++)h=e(l[r]),h in a||(a[h]=[]);this.filterClasses=a},s.prototype.update=function(i){this.itemHeightsDirty=!0,t.extend(!0,this,i)},s.prototype.onResize=function(){clearTimeout(this.resizeTimer),this.itemHeightsDirty=0!==this.flexibleWidth,this.resizeTimer=setTimeout(this.layout,this.resizeDelay)},s.prototype.onRefresh=function(){this.itemHeightsDirty=!0,this.layout()},s.prototype.filter=function(i,s,h){var o,n,r,a,l,f=[],u=t();if(i=i||[],s=s||"or",h=h||!1,i.length){for(n=0;i.length>n;n++)l=e(i[n]),l in this.filterClasses&&f.push(this.filterClasses[l]);if(o=f.length,"or"==s||1==o)for(n=0;o>n;n++)u=u.add(f[n]);else if("and"==s){var c,d,m,p=f[0],g=!0;for(n=1;o>n;n++)f[n].lengthn;n++){for(d=p[n],g=!0,r=0;f.length>r&&g;r++)if(m=f[r],p!=m){for(a=0,c=!1;m.length>a&&!c;a++)c=m[a]==d;g&=c}g&&u.push(p[n])}}h||this.handler.not(u).addClass("inactive")}else u=this.handler;return h||(u.removeClass("inactive"),this.columns=null,this.layout()),u},s.prototype.refreshPlaceholders=function(i,e){for(var s,h,o,n,r,a,l=this.placeholders.length,f=this.columns.length,u=this.container.innerHeight();f>l;l++)s=t('
          ').appendTo(this.container),this.placeholders.push(s);for(a=this.offset+2*parseInt(this.placeholders[0].css("borderLeftWidth"),10),l=0;this.placeholders.length>l;l++)if(s=this.placeholders[l],o=this.columns[l],l>=f||!o[o.length-1])s.css("display","none");else{if(h=o[o.length-1],!h)continue;r=h.data("wookmark-top")+h.data("wookmark-height")+this.verticalOffset,n=u-r-a,s.css({position:"absolute",display:n>0?"block":"none",left:l*i+e,top:r,width:i-a,height:n})}},s.prototype.getActiveItems=function(){return this.ignoreInactiveItems?this.handler.not(".inactive"):this.handler},s.prototype.getItemWidth=function(){var t=this.itemWidth,i=this.container.width()-2*this.outerOffset,e=this.handler.eq(0),s=this.flexibleWidth;if(void 0===this.itemWidth||0===this.itemWidth&&!this.flexibleWidth?t=e.outerWidth():"string"==typeof this.itemWidth&&this.itemWidth.indexOf("%")>=0&&(t=parseFloat(this.itemWidth)/100*i),s){"string"==typeof s&&s.indexOf("%")>=0&&(s=parseFloat(s)/100*i);var h=i+this.offset,o=~~(.5+h/(s+this.offset)),n=~~(h/(t+this.offset)),r=Math.max(o,n),a=Math.min(s,~~((i-(r-1)*this.offset)/r));t=Math.max(t,a),this.handler.css("width",t)}return t},s.prototype.layout=function(t){if(this.container.is(":visible")){var i,e=this.getItemWidth()+this.offset,s=this.container.width(),h=s-2*this.outerOffset,o=~~((h+this.offset)/e),n=0,r=0,a=0,l=this.getActiveItems(),f=l.length;if(this.itemHeightsDirty||!this.container.data("itemHeightsInitialized")){for(;f>a;a++)i=l.eq(a),i.data("wookmark-height",i.outerHeight());this.itemHeightsDirty=!1,this.container.data("itemHeightsInitialized",!0)}o=Math.max(1,Math.min(o,f)),n=this.outerOffset,"center"==this.align&&(n+=~~(.5+(h-(o*e-this.offset))>>1)),this.direction=this.direction||("right"==this.align?"right":"left"),r=t||null===this.columns||this.columns.length!=o||this.activeItemCount!=f?this.layoutFull(e,o,n):this.layoutColumns(e,n),this.activeItemCount=f,this.container.css("height",r),this.fillEmptySpace&&this.refreshPlaceholders(e,n),void 0!==this.onLayoutChanged&&"function"==typeof this.onLayoutChanged&&this.onLayoutChanged()}},s.prototype.sortElements=function(t){return"function"==typeof this.comparator?t.sort(this.comparator):t},s.prototype.layoutFull=function(e,s,h){var o,n,r=0,a=0,l=t.makeArray(this.getActiveItems()),f=l.length,u=null,c=null,d=[],m=[],p="left"==this.align?!0:!1;for(this.columns=[],l=this.sortElements(l);s>d.length;)d.push(this.outerOffset),this.columns.push([]);for(;f>r;r++){for(o=t(l[r]),u=d[0],c=0,a=0;s>a;a++)u>d[a]&&(u=d[a],c=a);o.data("wookmark-top",u),n=h,(c>0||!p)&&(n+=c*e),(m[r]={obj:o,css:{position:"absolute",top:u}}).css[this.direction]=n,d[c]+=o.data("wookmark-height")+this.verticalOffset,this.columns[c].push(o)}return i(m),Math.max.apply(Math,d)},s.prototype.layoutColumns=function(t,e){for(var s,h,o,n,r=[],a=[],l=0,f=0,u=0;this.columns.length>l;l++){for(r.push(this.outerOffset),h=this.columns[l],n=l*t+e,s=r[l],f=0;h.length>f;f++,u++)o=h[f].data("wookmark-top",s),(a[u]={obj:o,css:{top:s}}).css[this.direction]=n,s+=o.data("wookmark-height")+this.verticalOffset;r[l]=s}return i(a),Math.max.apply(Math,r)},s.prototype.clear=function(){clearTimeout(this.resizeTimer),r.unbind("resize.wookmark",this.onResize),this.container.unbind("refreshWookmark",this.onRefresh),this.handler.wookmarkInstance=null},s}(),t.fn.wookmark=function(t){return this.wookmarkInstance?this.wookmarkInstance.update(t||{}):this.wookmarkInstance=new s(this,t||{}),this.wookmarkInstance.layout(!0),this.show()}}); -------------------------------------------------------------------------------- /ui/html/schemas/css/main.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 750px) { 2 | #schemas, #fields { 3 | width: 100% !important; 4 | } 5 | 6 | body, html { 7 | overflow-y: auto !important; 8 | } 9 | } 10 | 11 | body, html { 12 | height: 100%; 13 | margin: 0; 14 | overflow-y: hidden; 15 | font-family: 'Roboto Slab', sans-serif; 16 | } 17 | 18 | form header { 19 | margin: 0 0 20px 0; 20 | } 21 | 22 | form header div { 23 | font-size: 90%; 24 | color: #999; 25 | } 26 | 27 | form header h2 { 28 | margin: 0 0 5px 0; 29 | } 30 | 31 | form > div { 32 | clear: both; 33 | overflow: hidden; 34 | padding: 1px; 35 | margin: 0 0 10px 0; 36 | } 37 | 38 | form > div > fieldset > div > div { 39 | margin: 0 0 5px 0; 40 | } 41 | 42 | form > div > label { 43 | float: left; 44 | padding-right: 10px; 45 | } 46 | 47 | form > div > fieldset label { 48 | font-size: 90%; 49 | } 50 | 51 | fieldset { 52 | border: 0; 53 | padding: 0; 54 | } 55 | 56 | .missing { 57 | border-color: #a90f0a !important; 58 | } 59 | 60 | .page_wrapper { 61 | width: 100%; 62 | height: 100%; 63 | } 64 | 65 | .wmd-button-bar { 66 | height: 46px; 67 | clear: both; 68 | background-color: transparent; 69 | margin: 15px 0 0 0; 70 | padding: 0 12px; 71 | width: 100%; 72 | box-sizing: border-box; 73 | border: 1px solid #c8ccd0; 74 | text-align: center; 75 | } 76 | 77 | .wmd-input { 78 | -moz-tab-size: 4; 79 | -o-tab-size: 4; 80 | tab-size: 4; 81 | -moz-box-sizing: border-box; 82 | -webkit-box-sizing: border-box; 83 | box-sizing: border-box; 84 | resize: none; 85 | outline: 0 86 | } 87 | 88 | .wmd-input { 89 | height: 200px; 90 | height: calc(100% - 73px); 91 | min-height: 200px; 92 | line-height: 1.3; 93 | width: 100%; 94 | padding: 8px 10px; 95 | font-size: 14px; 96 | color: #3b4045; 97 | background: #FFF; 98 | border: 1px solid #c8ccd0; 99 | overflow-y: auto; 100 | font-family: sans-serif; 101 | border-top: 0; 102 | text-align: center; 103 | border-bottom: 0; 104 | } 105 | 106 | .wmd-input-left { 107 | height: calc(100% - 50px); 108 | } 109 | 110 | .wmd-container .grippie { 111 | width: 100%; /* fallback if needed */ 112 | width: calc(100% - 2px); 113 | } 114 | 115 | .grippie { 116 | border: 1px solid #d6d9dc; 117 | height: 9px; 118 | overflow: hidden; 119 | background-color: #eff0f1; 120 | margin-top: -4px; 121 | } 122 | 123 | .wmd-container { 124 | height: calc(100% - 45px); 125 | } 126 | 127 | #schemas, #fields { 128 | position: relative; 129 | width: 50%; 130 | float: left; 131 | vertical-align: top; 132 | height: 100%; 133 | } 134 | 135 | .background { 136 | position: relative; 137 | z-index: 1; 138 | font-size: 13px; 139 | margin: 5px 0; 140 | text-align: center; 141 | font-weight: 500; 142 | font-family: 'Open Sans', sans-serif; 143 | } 144 | 145 | .background:before { 146 | border-top: 2px solid #dfdfdf; 147 | content: ""; 148 | margin: 0 auto; 149 | position: absolute; 150 | top: 50%; 151 | left: 0; 152 | right: 0; 153 | bottom: 0; 154 | width: 75%; 155 | z-index: -1; 156 | } 157 | 158 | .background span { 159 | background: #fff; 160 | padding: 0 15px; 161 | } 162 | 163 | #hamburger-icon { 164 | width: 25px; 165 | height: 20px; 166 | position: absolute; 167 | margin: 5px; 168 | z-index: 2; 169 | } 170 | 171 | #hamburger-icon .line { 172 | display: block; 173 | background: #333; 174 | width: 25px; 175 | height: 4px; 176 | position: absolute; 177 | left: 0; 178 | border-radius: 4px; 179 | transition: all 0.4s; 180 | -webkit-transition: all 0.4s; 181 | -moz-transition: all 0.4s; 182 | } 183 | 184 | #hamburger-icon .line.line-1 { 185 | top: 0; 186 | } 187 | 188 | #hamburger-icon .line.line-2 { 189 | top: 50%; 190 | } 191 | 192 | #hamburger-icon .line.line-3 { 193 | top: 100%; 194 | } 195 | 196 | #hamburger-icon.active .line-1 { 197 | transform: translateY(10px) translateX(0) rotate(45deg); 198 | -webkit-transform: translateY(10px) translateX(0) rotate(45deg); 199 | -moz-transform: translateY(10px) translateX(0) rotate(45deg); 200 | } 201 | 202 | #hamburger-icon.active .line-2 { 203 | opacity: 0; 204 | } 205 | 206 | #hamburger-icon.active .line-3 { 207 | transform: translateY(-10px) translateX(0) rotate(-45deg); 208 | -webkit-transform: translateY(-10px) translateX(0) rotate(-45deg); 209 | -moz-transform: translateY(-10px) translateX(0) rotate(-45deg); 210 | } 211 | 212 | #overlay { 213 | display: none; 214 | height: 100vh; 215 | width: 100%; 216 | position: fixed; 217 | z-index: 99; 218 | opacity: 0; 219 | background-color: rgba(0, 0, 0, 0.4); 220 | bottom: 0; 221 | right: 0; 222 | left: 0; 223 | } 224 | 225 | #overlay.open { 226 | display: block; 227 | } 228 | 229 | #hamburger-menu { 230 | height: 0; 231 | width: 0; 232 | background: #111; 233 | position: fixed; 234 | top: 5px; 235 | left: 35px; 236 | z-index: 101; 237 | overflow: hidden; 238 | border-radius: 5px; 239 | } 240 | 241 | #hamburger-menu nav { 242 | padding: 10px; 243 | z-index: 101; 244 | overflow-y: hidden; 245 | overflow-x: hidden; 246 | } 247 | 248 | #hamburger-menu nav a { 249 | display: block; 250 | padding: 10px; 251 | height: 1em; 252 | color: #CCC; 253 | font-size: 1em; 254 | line-height: 1em; 255 | text-decoration: none; 256 | white-space: nowrap; 257 | overflow: hidden; 258 | } 259 | 260 | #hamburger-menu nav a:hover { 261 | cursor: pointer; 262 | color: #FFF; 263 | background: rgba(255, 255, 255, 0.15); 264 | } 265 | 266 | .schema:hover, .topic:hover { 267 | background: #eee; 268 | } 269 | 270 | .schema, .topic { 271 | display: inline-block; 272 | border: 1px solid #ddd; 273 | margin: 0 5px 10px; 274 | min-width: 250px; 275 | padding: 10px; 276 | vertical-align: top; 277 | cursor: pointer; 278 | } 279 | 280 | .title_schema, .title_topic { 281 | margin: 0; 282 | color: #3232ff; 283 | font-weight: bold; 284 | } 285 | 286 | .delete_icon { 287 | position: relative; 288 | float: right; 289 | width: 12px; 290 | cursor: pointer; 291 | right: -6px; 292 | top: -6px; 293 | } 294 | 295 | .disabled_schema { 296 | background: #eee; 297 | opacity: 0.7; 298 | } 299 | 300 | #schema_title { 301 | text-align: center; 302 | background: #ddd; 303 | font-size: 16px; 304 | border-bottom: 1px solid #c8ccd0; 305 | } 306 | 307 | #schema_name { 308 | font-weight: bold; 309 | } 310 | 311 | #fields_result { 312 | height: calc(100% - 72px); 313 | text-align: left; 314 | } 315 | 316 | .schema_num, .topic_id { 317 | padding: 2px 10px 2px 12px; 318 | background: #47b8e5; 319 | color: #fff; 320 | text-decoration: none; 321 | font-size: 12px; 322 | font-weight: bold; 323 | word-break: break-all; 324 | } 325 | 326 | .highlight_schema { 327 | background: #AFC2D5 !important; 328 | } 329 | 330 | .btn { 331 | display: inline-block; 332 | margin-bottom: 0; 333 | outline: 0; 334 | line-height: 1.42857143; 335 | text-align: center; 336 | white-space: nowrap; 337 | vertical-align: middle; 338 | touch-action: manipulation; 339 | cursor: pointer; 340 | -webkit-user-select: none; 341 | -moz-user-select: none; 342 | -ms-user-select: none; 343 | user-select: none; 344 | border: 0; 345 | border-radius: 3px; 346 | margin-top: 10px; 347 | padding: 4px 12px; 348 | font-size: 14px; 349 | background: #3498db; 350 | color: white; 351 | position: relative; 352 | top: -2px; 353 | } 354 | 355 | button:disabled { 356 | background-color: graytext !important; 357 | opacity: 0.4; 358 | color: black; 359 | cursor: default; 360 | } 361 | 362 | .reveal-modal-bg { 363 | position: fixed; 364 | height: 100%; 365 | width: 100%; 366 | background: #000; 367 | background: rgba(0, 0, 0, .8); 368 | z-index: 100; 369 | display: none; 370 | top: 0; 371 | left: 0 372 | } 373 | 374 | .reveal-modal { 375 | visibility: hidden; 376 | top: 100px; 377 | width: 600px; 378 | background: #eee; 379 | position: absolute; 380 | z-index: 101; 381 | padding: 30px 40px 34px; 382 | -moz-border-radius: 5px; 383 | -webkit-border-radius: 5px; 384 | border-radius: 5px; 385 | max-width: 95%; 386 | left: 0; 387 | right: 0; 388 | margin-left: auto; 389 | margin-right: auto 390 | } 391 | 392 | .reveal-modal .close-reveal-modal, .close-info { 393 | font-size: 22px; 394 | line-height: .5; 395 | position: absolute; 396 | top: 8px; 397 | right: 11px; 398 | color: #aaa; 399 | font-weight: bold; 400 | cursor: pointer 401 | } 402 | 403 | #zero_fields { 404 | font-weight: bold; 405 | font-size: 22px; 406 | position: absolute; 407 | top: 50%; 408 | margin-top: -15px; 409 | display: none; 410 | text-align: center; 411 | left: 50%; 412 | margin-left: -57px; 413 | } 414 | 415 | #fields_result p { 416 | margin: 0 0 0 3px; 417 | font-weight: normal; 418 | } 419 | 420 | #fields_result ul li { 421 | padding-bottom: 5px; 422 | margin: 10px 0; 423 | font-weight: bold; 424 | font-size: 18px; 425 | color: #47b8e5; 426 | } 427 | 428 | #fields_result ul { 429 | padding-left: 20px; 430 | } 431 | 432 | #fields_result ul li:hover { 433 | background-color: #eee; 434 | } 435 | 436 | .delete_field { 437 | width: 10px; 438 | margin-left: 6px; 439 | position: relative; 440 | cursor: pointer; 441 | } 442 | 443 | #delete_message { 444 | display: none; 445 | position: fixed; 446 | top: 0; 447 | width: 100%; 448 | height: 100%; 449 | text-align: center; 450 | z-index: 3; 451 | background: rgba(0, 0, 0, 0.4); 452 | } 453 | 454 | #delete_message_title { 455 | height: 82px; 456 | margin-top: -16px; 457 | background-color: #5882FA; 458 | } 459 | 460 | #delete_message h2 { 461 | color: #0431B4; 462 | font-size: 20px; 463 | } 464 | 465 | #delete_edit, #cancel_delete { 466 | background: none; 467 | width: 100px; 468 | height: 28px; 469 | font-size: 16px; 470 | border-radius: 5px; 471 | border: 2px solid white; 472 | margin: 0 10px; 473 | cursor: pointer; 474 | color: #CED8F6; 475 | } 476 | 477 | .field { 478 | font-weight: normal; 479 | color: #333; 480 | font-size: 15px; 481 | } 482 | 483 | .deleted_selection, .deleted_selection_schema { 484 | background-color: #eee; 485 | } 486 | 487 | .property { 488 | font-weight: bold; 489 | } 490 | 491 | .type-boolean { 492 | color: firebrick; 493 | } 494 | 495 | ul { 496 | list-style: none; 497 | padding: 0; 498 | margin: 0; 499 | } 500 | 501 | li { 502 | padding-left: 1em; 503 | text-indent: -.7em; 504 | } 505 | 506 | li::before { 507 | content: "\002022"; 508 | font-family: "Arial Black"; 509 | color: black; /* or whatever color you prefer */ 510 | padding-right: 7px; 511 | } -------------------------------------------------------------------------------- /ui/html/schemas/imgs/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/schemas/imgs/delete.png -------------------------------------------------------------------------------- /ui/html/schemas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IPTC 5 | 6 | 7 | 8 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 |
          19 |
          20 |

          Are You Sure You Want To Delete This

          21 | 22 | 23 |
          24 |
          25 |
          26 |
          27 |
          28 |

          Create Schema

          29 | 30 |
          Fill the form
          31 |
          32 | 33 |
          34 | 35 | 36 |
          37 | 38 |
          39 |
          40 |
          41 | 44 | 45 |
          46 | 50 |
          51 |
          52 |
          53 | 54 |
          55 |
          56 | × 57 |
          58 |
          59 |
          60 |
          61 |

          Create Field

          62 | 63 |
          Fill the form
          64 |
          65 | 66 |
          67 | 68 | 69 |
          70 | 71 |
          72 |
          73 |
          74 | 77 | 78 |
          79 | 83 |
          84 |
          85 |
          86 | 89 | 90 |
          91 | 95 |
          96 |
          97 |
          98 | 101 | 102 |
          103 | 107 |
          108 |
          109 |
          110 | 111 |
          112 |
          113 | × 114 |
          115 | 116 | 117 | 118 | 119 | 120 |
          121 | 128 |
          129 |
          130 |
          131 |
          132 |

          SCHEMAS

          133 | 134 |
          135 |
          136 | 137 |
          138 |
          139 |
          140 |
          141 |
          142 |
          143 |

          FIELDS

          144 | 145 |
          146 |
          147 | 148 |
          149 |
          Schema: -
          150 |
          151 |
            152 |
          153 |
          154 |
          Zero fields
          155 |
          156 |
          157 |
          158 | 159 |
          160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /ui/html/schemas/js/jquery.reveal.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | 4 | $.fn.reveal = function () { 5 | 6 | 7 | var defaults = { 8 | animation: 'fadeAndPop', 9 | animationspeed: 300, 10 | closeonbackgroundclick: false, 11 | dismissmodalclass: 'close-reveal-modal' 12 | }; 13 | 14 | var options = $.extend({}, defaults, options); 15 | 16 | return this.each(function () { 17 | 18 | 19 | var modal = $(this), 20 | topMeasure = parseInt(modal.css('top')), 21 | topOffset = modal.height() + topMeasure, 22 | locked = false, 23 | modalBG = $('.reveal-modal-bg'); 24 | 25 | 26 | if (modalBG.length == 0) { 27 | modalBG = $('
          ').insertAfter(modal); 28 | } 29 | 30 | 31 | modal.bind('reveal:open', function () { 32 | modalBG.unbind('click.modalEvent'); 33 | $('.' + options.dismissmodalclass).unbind('click.modalEvent'); 34 | if (!locked) { 35 | lockModal(); 36 | modal.css({ 37 | 'top': 50,//$(document).scrollTop() - topOffset, 38 | 'opacity': 0, 39 | 'visibility': 'visible' 40 | }); 41 | modalBG.fadeIn(options.animationspeed / 2); 42 | modal.delay(options.animationspeed / 2).animate({ 43 | "top": '50px',//$(document).scrollTop() + topMeasure + 'px', 44 | "opacity": 1 45 | }, options.animationspeed, unlockModal()); 46 | } 47 | modal.unbind('reveal:open'); 48 | }); 49 | 50 | 51 | modal.bind('reveal:close', function () { 52 | 53 | if (!locked) { 54 | lockModal(); 55 | modalBG.delay(options.animationspeed).fadeOut(options.animationspeed); 56 | modal.animate({ 57 | "top": 50,// $(document).scrollTop() - topOffset + 'px', 58 | "opacity": 0 59 | }, options.animationspeed / 2, function () { 60 | modal.css({ 61 | 'top': 50,//topMeasure, 62 | 'opacity': 1, 63 | 'visibility': 'hidden' 64 | }); 65 | unlockModal(); 66 | }); 67 | } 68 | modal.unbind('reveal:close'); 69 | if ($(this).attr('id') === "myModal") { 70 | var id = $('#myModal').find('.media_topic').find('img').eq(0).attr('data-parent_id'); 71 | $('li[data-id="' + id + '"]').find('.media_topic').eq(0).html($('#myModal').find('.media_topic').eq(0).html()); 72 | $('li[data-id="' + id + '"]').find('.media_topic').eq(1).html($('#myModal').find('.media_topic').eq(1).html()); 73 | } 74 | }); 75 | 76 | 77 | modal.trigger('reveal:open') 78 | 79 | 80 | var closeButton = $('.' + options.dismissmodalclass).bind('click.modalEvent', function () { 81 | modal.trigger('reveal:close') 82 | }); 83 | 84 | if (options.closeonbackgroundclick) { 85 | modalBG.bind('click.modalEvent', function () { 86 | modal.trigger('reveal:close') 87 | }); 88 | } 89 | $('body').keyup(function (e) { 90 | if (e.which === 27) { 91 | modal.trigger('reveal:close'); 92 | } 93 | }); 94 | 95 | function unlockModal() { 96 | locked = false; 97 | } 98 | 99 | function lockModal() { 100 | locked = true; 101 | } 102 | 103 | }); 104 | } 105 | })(jQuery); -------------------------------------------------------------------------------- /ui/html/schemas/js/main.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $.ajax({ 3 | type: "GET", 4 | url: "http://" + window.location.hostname + ":8888/extra/api/schemas?nPerPage=100", 5 | dataType: "json", 6 | success: function (json) { 7 | for (var i = 0; i < json.entries.length; i++) { 8 | $('#schemas_result').append('

          ' + json.entries[i].name + '-' + json.entries[i].language + '

          fields: ' + json.entries[i].fieldNames.length + '

          ') 9 | } 10 | }, 11 | async: true 12 | }); 13 | var width = 220, 14 | height = 40 * 5, 15 | speed = 300, 16 | button = $('#hamburger-icon'), 17 | overlay = $('#overlay'), 18 | menu = $('#hamburger-menu'); 19 | 20 | button.on('click', function (e) { 21 | if (overlay.hasClass('open')) { 22 | animate_menu('close'); 23 | } else { 24 | animate_menu('open'); 25 | } 26 | }); 27 | 28 | overlay.on('click', function (e) { 29 | if (overlay.hasClass('open')) { 30 | animate_menu('close'); 31 | } 32 | }); 33 | 34 | $('a[href="#"]').on('click', function (e) { 35 | e.preventDefault(); 36 | }); 37 | 38 | function animate_menu(menu_toggle) { 39 | $('#hamburger-icon').toggleClass('active'); 40 | if (menu_toggle == 'open') { 41 | overlay.addClass('open'); 42 | button.addClass('on'); 43 | overlay.animate({opacity: 1}, speed); 44 | menu.animate({width: width, height: height}, speed); 45 | } 46 | 47 | if (menu_toggle == 'close') { 48 | button.removeClass('on'); 49 | overlay.animate({opacity: 0}, speed); 50 | overlay.removeClass('open'); 51 | menu.animate({width: "0", height: 0}, speed); 52 | } 53 | } 54 | 55 | $("#schemas_result").on("click", ".schema", function () { 56 | $('#zero_fields').hide(); 57 | $('#fields_result ul').empty(); 58 | $.ajax({ 59 | type: "GET", 60 | url: "http://" + window.location.hostname + ":8888/extra/api/schemas/" + $(this).attr('data-id'), 61 | dataType: "json", 62 | success: function (json) { 63 | $('#new_field').removeAttr('disabled'); 64 | for (var i = 0; i < json.fields.length; i++) { 65 | $('#fields_result ul').append('
        • ' + json.fields[i].name + '
          textual: ' + json.fields[i].textual + ',
          hasSentences: ' + json.fields[i].hasSentences + ',
          hasParagraphs: ' + json.fields[i].hasParagraphs + '
        • '); 66 | } 67 | $('#schemas_result').removeClass('disabled_schema'); 68 | if (json.fields.length === 0) { 69 | $('#zero_fields').show(); 70 | } 71 | }, 72 | async: true 73 | }); 74 | $('#schema_name').html($(this).find('.title_schema').text()); 75 | $('.schema').removeClass('highlight_schema'); 76 | $(this).addClass('highlight_schema'); 77 | }); 78 | }); 79 | 80 | $('#new_schema').click(function () { 81 | $('#new_schema_name').removeClass('missing').val(""); 82 | $('#myModal').reveal(); 83 | }); 84 | $("#schemas_result").on("click", ".delete_icon", function (e) { 85 | e.stopPropagation(); 86 | $(this).parent('div').addClass('deleted_selection_schema'); 87 | $("#delete_edit").attr('data-ref', 'schema'); 88 | $("#delete_message").slideDown(); 89 | }); 90 | $("#delete_edit").click(function () { 91 | if ($(this).attr('data-ref') === "field") { 92 | var viewData = { 93 | "name": $('#schema_name').text().substr(0, $('#schema_name').text().lastIndexOf('-')), 94 | "language": $('#schema_name').text().substr($('#schema_name').text().lastIndexOf('-') + 1), 95 | "fields": [] 96 | }; 97 | 98 | $("#fields_result ul li:not(.deleted_selection)").each(function () { 99 | viewData.fields.push({ 100 | "name": $(this).find('.field_title').text(), 101 | "textual": $(this).find('.field').eq(0).find('.type-boolean').text() == 'true', 102 | "hasSentences": $(this).find('.field').eq(1).find('.type-boolean').text() == 'true', 103 | "hasParagraphs": $(this).find('.field').eq(2).find('.type-boolean').text() == 'true' 104 | }); 105 | }); 106 | var data = JSON.stringify(viewData); 107 | console.log(data); 108 | $.ajax({ 109 | type: 'PUT', 110 | url: 'http://' + window.location.hostname + ':8888/extra/api/schemas/' + $('.highlight_schema').attr('data-id'), 111 | headers: { 112 | 'Accept': 'application/json', 113 | 'Content-Type': 'application/json' 114 | }, 115 | data: data, 116 | success: function () { 117 | $('.highlight_schema').find('.schema_num').text("fields: " + (($('.highlight_schema').find('.schema_num').text().split(' ')[1]) - 1)); 118 | $("#delete_message").slideUp(); 119 | $('.deleted_selection').remove(); 120 | if ($('#fields_result ul li').length === 0) { 121 | $('#zero_fields').show(); 122 | } 123 | }, 124 | error: function (e) { 125 | } 126 | }); 127 | } 128 | else { 129 | $.ajax({ 130 | type: 'DELETE', 131 | url: 'http://' + window.location.hostname + ':8888/extra/api/schemas/' + $('.deleted_selection_schema').attr('data-id'), 132 | success: function () { 133 | $("#delete_message").slideUp(); 134 | if ($('.deleted_selection_schema').hasClass('highlight_taxonomy')) { 135 | $('#schema_name').text('-'); 136 | $('#fields_result').addClass('disabled_schema'); 137 | $('#fields_result ul').empty(); 138 | $('#zero_fields').hide(); 139 | $('#new_field').attr('disabled', true); 140 | } 141 | $('.deleted_selection_schema').remove(); 142 | }, 143 | error: function (e) { 144 | } 145 | }); 146 | } 147 | }); 148 | $("#cancel_delete").click(function () { 149 | $("#delete_message").slideUp(); 150 | $('.deleted_selection').removeClass('deleted_selection'); 151 | $('.deleted_selection_schema').removeClass('deleted_selection_schema'); 152 | }); 153 | $('#create_schema').click(function () { 154 | if ($('#new_schema_name').val() !== "") { 155 | var viewData = { 156 | "name": $('#new_schema_name').val(), 157 | "language": $('#schema_lang').val() 158 | }; 159 | var data = JSON.stringify(viewData); 160 | $.ajax({ 161 | type: 'POST', 162 | url: 'http://' + window.location.hostname + ':8888/extra/api/schemas', 163 | headers: { 164 | 'Accept': 'application/json', 165 | 'Content-Type': 'application/json' 166 | }, 167 | data: data, 168 | success: function (json) { 169 | $('#schemas_result').prepend('

          ' + json.name + '-' + json.language + '

          fields: 0

          '); 170 | $('.close-reveal-modal').click(); 171 | }, 172 | error: function (e) { 173 | } 174 | }); 175 | 176 | $('.close-reveal-modal').click(); 177 | } 178 | else { 179 | $('#new_schema_name').addClass('missing'); 180 | } 181 | }); 182 | $('#new_field').click(function () { 183 | $('#new_field_name').removeClass('missing').val(""); 184 | $('#myModal2').reveal(); 185 | }); 186 | $('#create_field').click(function () { 187 | if ($('#new_field_name').val() !== "") { 188 | var viewData = { 189 | "name": $('#schema_name').text().substr(0, $('#schema_name').text().lastIndexOf('-')), 190 | "language": $('#schema_name').text().substr($('#schema_name').text().lastIndexOf('-') + 1), 191 | "fields": [] 192 | }; 193 | 194 | $("#fields_result ul li").each(function () { 195 | viewData.fields.push({ 196 | "name": $(this).find('.field_title').text(), 197 | "textual": $(this).find('.field').eq(0).find('.type-boolean').text() == 'true', 198 | "hasSentences": $(this).find('.field').eq(1).find('.type-boolean').text() == 'true', 199 | "hasParagraphs": $(this).find('.field').eq(2).find('.type-boolean').text() == 'true' 200 | }); 201 | }); 202 | viewData.fields.push({ 203 | "name": $('#new_field_name').val(), 204 | "textual": $('#field_textual').val() == 'true', 205 | "hasSentences": $('#field_sentence').val() == 'true', 206 | "hasParagraphs": $('#field_paragraphs').val() == 'true' 207 | }); 208 | var data = JSON.stringify(viewData); 209 | $.ajax({ 210 | type: 'PUT', 211 | url: 'http://' + window.location.hostname + ':8888/extra/api/schemas/' + $('.highlight_schema').attr('data-id'), 212 | headers: { 213 | 'Accept': 'application/json', 214 | 'Content-Type': 'application/json' 215 | }, 216 | data: data, 217 | success: function () { 218 | $('#fields_result ul').prepend('
        • ' + $('#new_field_name').val() + '
          textual: ' + $('#field_textual').val() + ',
          hasSentences: ' + $('#field_sentence').val() + ',
          hasParagraphs: ' + $('#field_paragraphs').val() + '
        • '); 219 | $('.close-reveal-modal').click(); 220 | $('#zero_fields').hide(); 221 | $('.highlight_schema').find('.schema_num').text("fields: " + (parseInt(($('.highlight_schema').find('.schema_num').text().split(' ')[1])) + 1)); 222 | }, 223 | error: function (e) { 224 | } 225 | }); 226 | } else { 227 | $('#new_field_name').addClass('missing'); 228 | } 229 | }); 230 | $(document).ready(function () { 231 | $("form").bind("keypress", function (e) { 232 | if (e.keyCode == 13) { 233 | return false; 234 | } 235 | }); 236 | }); 237 | $("#fields_result").on("click", ".delete_field", function (e) { 238 | $(this).parent('li').addClass('deleted_selection'); 239 | $("#delete_edit").attr('data-ref', 'field'); 240 | $("#delete_message").slideDown(); 241 | }); -------------------------------------------------------------------------------- /ui/html/tagging/imgs/add-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/tagging/imgs/add-icon.png -------------------------------------------------------------------------------- /ui/html/tagging/imgs/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/tagging/imgs/delete.png -------------------------------------------------------------------------------- /ui/html/tagging/imgs/search-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/tagging/imgs/search-12.png -------------------------------------------------------------------------------- /ui/html/tagging/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IPTC 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
          28 | 37 |
          38 |
          39 |

          Document

          40 | 41 |
          42 |
          43 |
            44 |
          • 45 | 46 |
          • 47 |
          • 48 | 51 |
          • 52 |
          • 53 | 56 |
          • 57 |
          58 |
          59 |
          60 | × 61 | Invalid!
          62 |
          63 |
          65 |
          66 |
          67 |
          68 |
          69 |

          RULES

          70 |
          71 |
          72 |
          73 |
          74 |
          Zero rules
          75 |
          76 |
          77 |
            78 |
            79 |
            80 |
            81 |
            82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ui/html/tagging/js/jquery.twbsPagination.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Bootstrap Pagination v1.4.1 3 | * https://github.com/esimakin/twbs-pagination 4 | * 5 | * Copyright 2014-2016, Eugene Simakin 6 | * Released under Apache-2.0 license 7 | * http://apache.org/licenses/LICENSE-2.0.html 8 | */ 9 | !function(a,b,c,d){"use strict";var e=a.fn.twbsPagination,f=function(b,c){if(this.$element=a(b),this.options=a.extend({},a.fn.twbsPagination.defaults,c),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw new Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw new Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw new Error("Visible pages option is not correct!");if(this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.$element.trigger("page",1),this;this.options.totalPages"),this.$listContainer.addClass(this.options.paginationClass),"UL"!==d&&this.$element.append(this.$listContainer),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.currentPage=this.options.startPage,this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};f.prototype={constructor:f,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(a){if(a<1||a>this.options.totalPages)throw new Error("Page is incorrect.");return this.currentPage=a,this.render(this.getPages(a)),this.setupEvents(),this.$element.trigger("page",a),this},enable:function(){this.show(this.currentPage)},disable:function(){var b=this;this.$listContainer.off("click").on("click","li",function(a){a.preventDefault()}),this.$listContainer.children().each(function(){var c=a(this);c.hasClass(b.options.activeClass)||a(this).addClass(b.options.disabledClass)})},buildListItems:function(a){var b=[];if(this.options.first&&b.push(this.buildItem("first",1)),this.options.prev){var c=a.currentPage>1?a.currentPage-1:this.options.loop?this.options.totalPages:1;b.push(this.buildItem("prev",c))}for(var d=0;d"),e=a(""),f=this.options[b]?this.makeText(this.options[b],c):c;return d.addClass(this.options[b+"Class"]),d.data("page",c),d.data("page-type",b),d.append(e.attr("href",this.makeHref(c)).addClass(this.options.anchorClass).html(f)),d},getPages:function(a){var b=[],c=Math.floor(this.options.visiblePages/2),d=a-c+1-this.options.visiblePages%2,e=a+c;d<=0&&(d=1,e=this.options.visiblePages),e>this.options.totalPages&&(d=this.options.totalPages-this.options.visiblePages+1,e=this.options.totalPages);for(var f=d;f<=e;)b.push(f),f++;return{currentPage:a,numeric:b}},render:function(b){var c=this;this.$listContainer.children().remove();var d=this.buildListItems(b);a.each(d,function(a,b){c.$listContainer.append(b)}),this.$listContainer.children().each(function(){var d=a(this),e=d.data("page-type");switch(e){case"page":d.data("page")===b.currentPage&&d.addClass(c.options.activeClass);break;case"first":d.toggleClass(c.options.disabledClass,1===b.currentPage);break;case"last":d.toggleClass(c.options.disabledClass,b.currentPage===c.options.totalPages);break;case"prev":d.toggleClass(c.options.disabledClass,!c.options.loop&&1===b.currentPage);break;case"next":d.toggleClass(c.options.disabledClass,!c.options.loop&&b.currentPage===c.options.totalPages)}})},setupEvents:function(){var b=this;this.$listContainer.off("click").on("click","li",function(c){var d=a(this);return!d.hasClass(b.options.disabledClass)&&!d.hasClass(b.options.activeClass)&&(!b.options.href&&c.preventDefault(),void b.show(parseInt(d.data("page"))))})},makeHref:function(a){return this.options.href?this.generateQueryString(a):"#"},makeText:function(a,b){return a.replace(this.options.pageVariable,b).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(a){var b=this.getSearchString(a),c=new RegExp(this.options.pageVariable+"(=([^&#]*)|&|#|$)"),d=c.exec(b);return d&&d[2]?(d=decodeURIComponent(d[2]),d=parseInt(d),isNaN(d)?null:d):null},generateQueryString:function(a,b){var c=this.getSearchString(b),d=new RegExp(this.options.pageVariable+"=*[^&#]*");return c?"?"+c.replace(d,this.options.pageVariable+"="+a):""},getSearchString:function(a){var c=a||b.location.search;return""===c?null:(0===c.indexOf("?")&&(c=c.substr(1)),c)},getCurrentPage:function(){return this.currentPage}},a.fn.twbsPagination=function(b){var c,e=Array.prototype.slice.call(arguments,1),g=a(this),h=g.data("twbs-pagination"),i="object"==typeof b?b:{};return h||g.data("twbs-pagination",h=new f(this,i)),"string"==typeof b&&(c=h[b].apply(h,e)),c===d?g:c},a.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},a.fn.twbsPagination.Constructor=f,a.fn.twbsPagination.noConflict=function(){return a.fn.twbsPagination=e,this},a.fn.twbsPagination.version="1.4.1"}(window.jQuery,window,document); -------------------------------------------------------------------------------- /ui/html/tagging/js/jquery.wookmark.js: -------------------------------------------------------------------------------- 1 | (function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function i(t){n(function(){var i,e;for(i=0;t.length>i;i++)e=t[i],e.obj.css(e.css)})}function e(i){return t.trim(i).toLowerCase()}var s,h,o;o=function(t,i){return function(){return t.apply(i,arguments)}},h={align:"center",autoResize:!1,comparator:null,container:t("body"),direction:void 0,ignoreInactiveItems:!0,itemWidth:0,fillEmptySpace:!1,flexibleWidth:0,offset:2,outerOffset:0,onLayoutChanged:void 0,possibleFilters:[],resizeDelay:50,verticalOffset:void 0};var n=window.requestAnimationFrame||function(t){t()},r=t(window);s=function(){function s(i,e){this.handler=i,this.columns=this.containerWidth=this.resizeTimer=null,this.activeItemCount=0,this.itemHeightsDirty=!0,this.placeholders=[],t.extend(!0,this,h,e),this.verticalOffset=this.verticalOffset||this.offset,this.update=o(this.update,this),this.onResize=o(this.onResize,this),this.onRefresh=o(this.onRefresh,this),this.getItemWidth=o(this.getItemWidth,this),this.layout=o(this.layout,this),this.layoutFull=o(this.layoutFull,this),this.layoutColumns=o(this.layoutColumns,this),this.filter=o(this.filter,this),this.clear=o(this.clear,this),this.getActiveItems=o(this.getActiveItems,this),this.refreshPlaceholders=o(this.refreshPlaceholders,this),this.sortElements=o(this.sortElements,this),this.updateFilterClasses=o(this.updateFilterClasses,this),this.updateFilterClasses(),this.autoResize&&r.bind("resize.wookmark",this.onResize),this.container.bind("refreshWookmark",this.onRefresh)}return s.prototype.updateFilterClasses=function(){for(var t,i,s,h,o=0,n=0,r=0,a={},l=this.possibleFilters;this.handler.length>o;o++)if(i=this.handler.eq(o),t=i.data("filterClass"),"object"==typeof t&&t.length>0)for(n=0;t.length>n;n++)s=e(t[n]),a[s]===void 0&&(a[s]=[]),a[s].push(i[0]);for(;l.length>r;r++)h=e(l[r]),h in a||(a[h]=[]);this.filterClasses=a},s.prototype.update=function(i){this.itemHeightsDirty=!0,t.extend(!0,this,i)},s.prototype.onResize=function(){clearTimeout(this.resizeTimer),this.itemHeightsDirty=0!==this.flexibleWidth,this.resizeTimer=setTimeout(this.layout,this.resizeDelay)},s.prototype.onRefresh=function(){this.itemHeightsDirty=!0,this.layout()},s.prototype.filter=function(i,s,h){var o,n,r,a,l,f=[],u=t();if(i=i||[],s=s||"or",h=h||!1,i.length){for(n=0;i.length>n;n++)l=e(i[n]),l in this.filterClasses&&f.push(this.filterClasses[l]);if(o=f.length,"or"==s||1==o)for(n=0;o>n;n++)u=u.add(f[n]);else if("and"==s){var c,d,m,p=f[0],g=!0;for(n=1;o>n;n++)f[n].lengthn;n++){for(d=p[n],g=!0,r=0;f.length>r&&g;r++)if(m=f[r],p!=m){for(a=0,c=!1;m.length>a&&!c;a++)c=m[a]==d;g&=c}g&&u.push(p[n])}}h||this.handler.not(u).addClass("inactive")}else u=this.handler;return h||(u.removeClass("inactive"),this.columns=null,this.layout()),u},s.prototype.refreshPlaceholders=function(i,e){for(var s,h,o,n,r,a,l=this.placeholders.length,f=this.columns.length,u=this.container.innerHeight();f>l;l++)s=t('
            ').appendTo(this.container),this.placeholders.push(s);for(a=this.offset+2*parseInt(this.placeholders[0].css("borderLeftWidth"),10),l=0;this.placeholders.length>l;l++)if(s=this.placeholders[l],o=this.columns[l],l>=f||!o[o.length-1])s.css("display","none");else{if(h=o[o.length-1],!h)continue;r=h.data("wookmark-top")+h.data("wookmark-height")+this.verticalOffset,n=u-r-a,s.css({position:"absolute",display:n>0?"block":"none",left:l*i+e,top:r,width:i-a,height:n})}},s.prototype.getActiveItems=function(){return this.ignoreInactiveItems?this.handler.not(".inactive"):this.handler},s.prototype.getItemWidth=function(){var t=this.itemWidth,i=this.container.width()-2*this.outerOffset,e=this.handler.eq(0),s=this.flexibleWidth;if(void 0===this.itemWidth||0===this.itemWidth&&!this.flexibleWidth?t=e.outerWidth():"string"==typeof this.itemWidth&&this.itemWidth.indexOf("%")>=0&&(t=parseFloat(this.itemWidth)/100*i),s){"string"==typeof s&&s.indexOf("%")>=0&&(s=parseFloat(s)/100*i);var h=i+this.offset,o=~~(.5+h/(s+this.offset)),n=~~(h/(t+this.offset)),r=Math.max(o,n),a=Math.min(s,~~((i-(r-1)*this.offset)/r));t=Math.max(t,a),this.handler.css("width",t)}return t},s.prototype.layout=function(t){if(this.container.is(":visible")){var i,e=this.getItemWidth()+this.offset,s=this.container.width(),h=s-2*this.outerOffset,o=~~((h+this.offset)/e),n=0,r=0,a=0,l=this.getActiveItems(),f=l.length;if(this.itemHeightsDirty||!this.container.data("itemHeightsInitialized")){for(;f>a;a++)i=l.eq(a),i.data("wookmark-height",i.outerHeight());this.itemHeightsDirty=!1,this.container.data("itemHeightsInitialized",!0)}o=Math.max(1,Math.min(o,f)),n=this.outerOffset,"center"==this.align&&(n+=~~(.5+(h-(o*e-this.offset))>>1)),this.direction=this.direction||("right"==this.align?"right":"left"),r=t||null===this.columns||this.columns.length!=o||this.activeItemCount!=f?this.layoutFull(e,o,n):this.layoutColumns(e,n),this.activeItemCount=f,this.container.css("height",r),this.fillEmptySpace&&this.refreshPlaceholders(e,n),void 0!==this.onLayoutChanged&&"function"==typeof this.onLayoutChanged&&this.onLayoutChanged()}},s.prototype.sortElements=function(t){return"function"==typeof this.comparator?t.sort(this.comparator):t},s.prototype.layoutFull=function(e,s,h){var o,n,r=0,a=0,l=t.makeArray(this.getActiveItems()),f=l.length,u=null,c=null,d=[],m=[],p="left"==this.align?!0:!1;for(this.columns=[],l=this.sortElements(l);s>d.length;)d.push(this.outerOffset),this.columns.push([]);for(;f>r;r++){for(o=t(l[r]),u=d[0],c=0,a=0;s>a;a++)u>d[a]&&(u=d[a],c=a);o.data("wookmark-top",u),n=h,(c>0||!p)&&(n+=c*e),(m[r]={obj:o,css:{position:"absolute",top:u}}).css[this.direction]=n,d[c]+=o.data("wookmark-height")+this.verticalOffset,this.columns[c].push(o)}return i(m),Math.max.apply(Math,d)},s.prototype.layoutColumns=function(t,e){for(var s,h,o,n,r=[],a=[],l=0,f=0,u=0;this.columns.length>l;l++){for(r.push(this.outerOffset),h=this.columns[l],n=l*t+e,s=r[l],f=0;h.length>f;f++,u++)o=h[f].data("wookmark-top",s),(a[u]={obj:o,css:{top:s}}).css[this.direction]=n,s+=o.data("wookmark-height")+this.verticalOffset;r[l]=s}return i(a),Math.max.apply(Math,r)},s.prototype.clear=function(){clearTimeout(this.resizeTimer),r.unbind("resize.wookmark",this.onResize),this.container.unbind("refreshWookmark",this.onRefresh),this.handler.wookmarkInstance=null},s}(),t.fn.wookmark=function(t){return this.wookmarkInstance?this.wookmarkInstance.update(t||{}):this.wookmarkInstance=new s(this,t||{}),this.wookmarkInstance.layout(!0),this.show()}}); -------------------------------------------------------------------------------- /ui/html/tagging/js/main.js: -------------------------------------------------------------------------------- 1 | $('#search_but').click(function () { 2 | try { 3 | var obj = JSON.parse($('#wmd-input').text()); 4 | var str = JSON.stringify(obj, undefined, 4); 5 | $('#wmd-input').html(syntaxHighlight(str)); 6 | $('#error_modal').slideUp(); 7 | $('#wmd-input').removeClass('error_open'); 8 | $('#result_rules').empty(); 9 | $.ajax({ 10 | type: 'POST', 11 | url: 'http://' + window.location.hostname + ':8888/extra/api/classifications?schemaId=' + $('#schema_select').val() + '&groupId=' + $('#group_select').val(), 12 | headers: { 13 | 'Accept': 'application/json', 14 | 'Content-Type': 'application/json' 15 | }, 16 | data: '{"document":' + str + '}', 17 | success: function (json) { 18 | for (var t = 0; t < json.entries.length; t++) { 19 | var d = new Date(json.entries[t].createdAt); 20 | d = ISODateString(d); 21 | var d2 = new Date(json.entries[t].updatedAt); 22 | d2 = ISODateString(d2); 23 | $('#result_rules').append('

            ' + json.entries[t].name + '

            Created at: ' + d + '

            Updated at: ' + d2 + '

            '); 24 | } 25 | if (json.total > 0) { 26 | $('#zero_rules').hide(); 27 | //$('.rule[data-id="' + $('#save_but').attr('data-id') + '"]').addClass('highlight_rule'); 28 | var $articles_pagination = $('#rules_pagination'); 29 | $('#well_rules').show(); 30 | $('#rules_list').addClass('open_well'); 31 | if ($articles_pagination.data("twbs-pagination")) { 32 | $articles_pagination.twbsPagination('destroy'); 33 | } 34 | var total_page = json.total / json.nPerPage; 35 | if (total_page % 1 != 0) { 36 | total_page = Math.floor(json.total / json.nPerPage) + 1 37 | } 38 | $articles_pagination.twbsPagination({ 39 | totalPages: total_page, 40 | initiateStartPageClick: false, 41 | startPage: 1, 42 | onPageClick: function (event, page) { 43 | $('#result_rules').empty(); 44 | parse_results(page); 45 | } 46 | }); 47 | var options = { 48 | autoResize: true, 49 | container: $('#result_rules'), 50 | offset: 10, 51 | itemWidth: 322, 52 | outerOffset: 0 53 | }; 54 | 55 | var handler = $('#result_rules > .rule'); 56 | handler.wookmark(options); 57 | } 58 | else { 59 | $('#zero_rules').show(); 60 | $('#well_rules').hide(); 61 | $('#rules_list').removeClass('open_well'); 62 | } 63 | }, 64 | error: function (e) { 65 | } 66 | }); 67 | } catch (ex) { 68 | $("#error_msg").text(ex); 69 | $('#error_modal').slideDown(); 70 | $('#wmd-input').addClass('error_open'); 71 | $('#well_rules').hide(); 72 | $('#rules_list').removeClass('open_well'); 73 | $('#result_rules').empty(); 74 | } 75 | }); 76 | function parse_results(page) { 77 | try { 78 | var obj = JSON.parse($('#wmd-input').text()); 79 | var str = JSON.stringify(obj, undefined, 4); 80 | $('#wmd-input').html(syntaxHighlight(str)); 81 | $('#error_modal').slideUp(); 82 | $('#wmd-input').removeClass('error_open'); 83 | $.ajax({ 84 | type: 'POST', 85 | url: 'http://' + window.location.hostname + ':8888/extra/api/classifications?page=' + page + 'schemaId=' + $('#schema_select').val() + '&groupId=' + $('#group_select').val(), 86 | headers: { 87 | 'Accept': 'application/json', 88 | 'Content-Type': 'application/json' 89 | }, 90 | data: str, 91 | success: function (json) { 92 | for (var t = 0; t < json.entries.length; t++) { 93 | var d = new Date(json.entries[t].createdAt); 94 | d = ISODateString(d); 95 | var d2 = new Date(json.entries[t].updatedAt); 96 | d2 = ISODateString(d2); 97 | $('#result_rules').append('

            ' + json.entries[t].name + '

            Created at: ' + d + '

            Updated at: ' + d2 + '

            '); 98 | } 99 | var options = { 100 | autoResize: true, 101 | container: $('#result_rules'), 102 | offset: 10, 103 | itemWidth: 322, 104 | outerOffset: 0 105 | }; 106 | 107 | var handler = $('#result_rules > .rule'); 108 | handler.wookmark(options); 109 | }, 110 | error: function (e) { 111 | } 112 | }); 113 | } catch (ex) { 114 | $("#error_msg").text(ex); 115 | $('#error_modal').slideDown(); 116 | $('#wmd-input').addClass('error_open'); 117 | $('#well_rules').hide(); 118 | $('#rules_list').removeClass('open_well'); 119 | $('#result_rules').empty(); 120 | } 121 | } 122 | $('[contenteditable]').on('focus', function () { 123 | var $this = $(this); 124 | $this.data('before', $this.html()); 125 | return $this; 126 | }).on('blur keyup paste', function () { 127 | var $this = $(this); 128 | if ($this.data('before') !== $this.html()) { 129 | $this.data('before', $this.html()); 130 | $this.trigger('change'); 131 | } 132 | return $this; 133 | }); 134 | $('#wmd-input').bind('change', function () { 135 | if ($('#schema_select').val()) { 136 | if ($(this).html() != "") { 137 | $('#search_but').attr('disabled', false); 138 | } 139 | else { 140 | $('#search_but').attr('disabled', true); 141 | } 142 | } 143 | }); 144 | $('#schema_select').on('change', function () { 145 | if ($("#wmd-input").html() !== "") { 146 | $('#search_but').attr('disabled', false); 147 | } 148 | }); 149 | $(function () { 150 | $.ajax({ 151 | type: "GET", 152 | url: "http://" + window.location.hostname + ":8888/extra/api/schemas", 153 | dataType: "json", 154 | success: function (json) { 155 | for (var i = 0; i < json.entries.length; i++) { 156 | $('#schema_select').append(''); 157 | } 158 | }, 159 | async: true 160 | }); 161 | $.ajax({ 162 | type: "GET", 163 | url: "http://" + window.location.hostname + ":8888/extra/api/rules/groups", 164 | dataType: "json", 165 | success: function (json) { 166 | for (var i = 0; i < json.entries.length; i++) { 167 | $('#group_select').append(''); 168 | } 169 | }, 170 | async: true 171 | }); 172 | var width = 220, 173 | height = 40 * 5, 174 | speed = 300, 175 | button = $('#hamburger-icon'), 176 | overlay = $('#overlay'), 177 | menu = $('#hamburger-menu'); 178 | 179 | button.on('click', function (e) { 180 | if (overlay.hasClass('open')) { 181 | animate_menu('close'); 182 | } else { 183 | animate_menu('open'); 184 | } 185 | }); 186 | 187 | overlay.on('click', function (e) { 188 | if (overlay.hasClass('open')) { 189 | animate_menu('close'); 190 | } 191 | }); 192 | 193 | $('a[href="#"]').on('click', function (e) { 194 | e.preventDefault(); 195 | }); 196 | 197 | function animate_menu(menu_toggle) { 198 | $('#hamburger-icon').toggleClass('active'); 199 | if (menu_toggle == 'open') { 200 | overlay.addClass('open'); 201 | button.addClass('on'); 202 | overlay.animate({opacity: 1}, speed); 203 | menu.animate({width: width, height: height}, speed); 204 | } 205 | 206 | if (menu_toggle == 'close') { 207 | button.removeClass('on'); 208 | overlay.animate({opacity: 0}, speed); 209 | overlay.removeClass('open'); 210 | menu.animate({width: "0", height: 0}, speed); 211 | } 212 | } 213 | }); 214 | function syntaxHighlight(json) { 215 | json = json.replace(/&/g, '&').replace(//g, '>'); 216 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 217 | var cls = 'number'; 218 | if (/^"/.test(match)) { 219 | if (/:$/.test(match)) { 220 | cls = 'key'; 221 | } else { 222 | cls = 'string'; 223 | } 224 | } else if (/true|false/.test(match)) { 225 | cls = 'boolean'; 226 | } else if (/null/.test(match)) { 227 | cls = 'null'; 228 | } 229 | return '' + match + ''; 230 | }); 231 | } 232 | $('#error_close').click(function () { 233 | $('#error_modal').slideUp(); 234 | $('#wmd-input').removeClass('error_open'); 235 | }); 236 | function ISODateString(d) { 237 | return pad(d.getUTCMonth() + 1) + '/' + pad(d.getUTCDate()) + '/' + d.getUTCFullYear(); 238 | } 239 | function pad(n) { 240 | return n < 10 ? '0' + n : n 241 | } -------------------------------------------------------------------------------- /ui/html/taxonomies/imgs/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/taxonomies/imgs/delete.png -------------------------------------------------------------------------------- /ui/html/taxonomies/imgs/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/taxonomies/imgs/edit.png -------------------------------------------------------------------------------- /ui/html/taxonomies/imgs/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/taxonomies/imgs/link.png -------------------------------------------------------------------------------- /ui/html/taxonomies/imgs/search-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptc/extra-ext/0c5eb6f4c0f40d719de023dab9df7259cd1d77e3/ui/html/taxonomies/imgs/search-12.png -------------------------------------------------------------------------------- /ui/html/taxonomies/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IPTC 5 | 6 | 7 | 8 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 |
            19 |
            20 |

            Are You Sure You Want To Delete This

            21 | 22 | 23 |
            24 |
            25 |
            26 |
            27 |
            28 |

            Create Taxonomy

            29 | 30 |
            Fill the form
            31 |
            32 | 33 |
            34 | 35 | 36 |
            37 | 38 |
            39 |
            40 |
            41 | 44 | 45 |
            46 | 52 |
            53 |
            54 |
            55 | 56 |
            57 |
            58 | × 59 |
            60 |
            61 |
            62 |
            63 |

            Create Topic

            64 | 65 |
            Fill the form
            66 |
            67 |
            68 | 69 | 70 |
            71 | 72 |
            73 |
            74 |
            75 | 76 | 77 |
            78 | 79 |
            80 |
            81 |
            82 | 83 | 84 |
            85 | 86 |
            87 |
            88 |
            89 | 90 |
            91 |
            92 | × 93 |
            94 |
            95 |
            96 |
            97 |

            Edit Topic

            98 | 99 |
            Fill the form
            100 |
            101 | 102 |
            103 | 104 | 105 |
            106 | 107 |
            108 |
            109 |
            110 | 111 | 112 |
            113 | 114 |
            115 |
            116 |
            117 | 118 | 119 |
            120 | 121 |
            122 |
            123 |
            124 | 125 |
            126 |
            127 | × 128 |
            129 | 130 | 131 | 132 | 133 | 134 | 135 |
            136 | 143 |
            144 |
            145 |
            146 |
            147 |

            TAXONOMIES

            148 | 149 |
            150 |
            151 | 152 |
            153 |
            154 |
            155 |
            156 |
            157 |
            158 |

            TOPICS

            159 | 160 |
            161 |
            162 | 163 | 164 | search-icon 165 |
            166 |
            Taxonomy: -
            167 |
            168 |
              169 |
            170 |
            171 |
            Zero topics
            172 |
            173 |
              174 |
              175 |
              176 |
              177 |
              178 | 179 |
              180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /ui/html/taxonomies/js/jquery.reveal.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | 4 | $.fn.reveal = function () { 5 | 6 | 7 | var defaults = { 8 | animation: 'fadeAndPop', 9 | animationspeed: 300, 10 | closeonbackgroundclick: false, 11 | dismissmodalclass: 'close-reveal-modal' 12 | }; 13 | 14 | var options = $.extend({}, defaults, options); 15 | 16 | return this.each(function () { 17 | 18 | 19 | var modal = $(this), 20 | topMeasure = parseInt(modal.css('top')), 21 | topOffset = modal.height() + topMeasure, 22 | locked = false, 23 | modalBG = $('.reveal-modal-bg'); 24 | 25 | 26 | if (modalBG.length == 0) { 27 | modalBG = $('
              ').insertAfter(modal); 28 | } 29 | 30 | 31 | modal.bind('reveal:open', function () { 32 | modalBG.unbind('click.modalEvent'); 33 | $('.' + options.dismissmodalclass).unbind('click.modalEvent'); 34 | if (!locked) { 35 | lockModal(); 36 | modal.css({ 37 | 'top': 50,//$(document).scrollTop() - topOffset, 38 | 'opacity': 0, 39 | 'visibility': 'visible' 40 | }); 41 | modalBG.fadeIn(options.animationspeed / 2); 42 | modal.delay(options.animationspeed / 2).animate({ 43 | "top": '50px',//$(document).scrollTop() + topMeasure + 'px', 44 | "opacity": 1 45 | }, options.animationspeed, unlockModal()); 46 | } 47 | modal.unbind('reveal:open'); 48 | }); 49 | 50 | 51 | modal.bind('reveal:close', function () { 52 | 53 | if (!locked) { 54 | lockModal(); 55 | modalBG.delay(options.animationspeed).fadeOut(options.animationspeed); 56 | modal.animate({ 57 | "top": 50,// $(document).scrollTop() - topOffset + 'px', 58 | "opacity": 0 59 | }, options.animationspeed / 2, function () { 60 | modal.css({ 61 | 'top': 50,//topMeasure, 62 | 'opacity': 1, 63 | 'visibility': 'hidden' 64 | }); 65 | unlockModal(); 66 | }); 67 | } 68 | modal.unbind('reveal:close'); 69 | if ($(this).attr('id') === "myModal") { 70 | var id = $('#myModal').find('.media_topic').find('img').eq(0).attr('data-parent_id'); 71 | $('li[data-id="' + id + '"]').find('.media_topic').eq(0).html($('#myModal').find('.media_topic').eq(0).html()); 72 | $('li[data-id="' + id + '"]').find('.media_topic').eq(1).html($('#myModal').find('.media_topic').eq(1).html()); 73 | } 74 | }); 75 | 76 | 77 | modal.trigger('reveal:open') 78 | 79 | 80 | var closeButton = $('.' + options.dismissmodalclass).bind('click.modalEvent', function () { 81 | modal.trigger('reveal:close') 82 | }); 83 | 84 | if (options.closeonbackgroundclick) { 85 | modalBG.bind('click.modalEvent', function () { 86 | modal.trigger('reveal:close') 87 | }); 88 | } 89 | $('body').keyup(function (e) { 90 | if (e.which === 27) { 91 | modal.trigger('reveal:close'); 92 | } 93 | }); 94 | 95 | function unlockModal() { 96 | locked = false; 97 | } 98 | 99 | function lockModal() { 100 | locked = true; 101 | } 102 | 103 | }); 104 | } 105 | })(jQuery); -------------------------------------------------------------------------------- /ui/html/taxonomies/js/jquery.twbsPagination.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Bootstrap Pagination v1.4.1 3 | * https://github.com/esimakin/twbs-pagination 4 | * 5 | * Copyright 2014-2016, Eugene Simakin 6 | * Released under Apache-2.0 license 7 | * http://apache.org/licenses/LICENSE-2.0.html 8 | */ 9 | !function(a,b,c,d){"use strict";var e=a.fn.twbsPagination,f=function(b,c){if(this.$element=a(b),this.options=a.extend({},a.fn.twbsPagination.defaults,c),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw new Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw new Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw new Error("Visible pages option is not correct!");if(this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.$element.trigger("page",1),this;this.options.totalPages"),this.$listContainer.addClass(this.options.paginationClass),"UL"!==d&&this.$element.append(this.$listContainer),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.currentPage=this.options.startPage,this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};f.prototype={constructor:f,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(a){if(a<1||a>this.options.totalPages)throw new Error("Page is incorrect.");return this.currentPage=a,this.render(this.getPages(a)),this.setupEvents(),this.$element.trigger("page",a),this},enable:function(){this.show(this.currentPage)},disable:function(){var b=this;this.$listContainer.off("click").on("click","li",function(a){a.preventDefault()}),this.$listContainer.children().each(function(){var c=a(this);c.hasClass(b.options.activeClass)||a(this).addClass(b.options.disabledClass)})},buildListItems:function(a){var b=[];if(this.options.first&&b.push(this.buildItem("first",1)),this.options.prev){var c=a.currentPage>1?a.currentPage-1:this.options.loop?this.options.totalPages:1;b.push(this.buildItem("prev",c))}for(var d=0;d"),e=a(""),f=this.options[b]?this.makeText(this.options[b],c):c;return d.addClass(this.options[b+"Class"]),d.data("page",c),d.data("page-type",b),d.append(e.attr("href",this.makeHref(c)).addClass(this.options.anchorClass).html(f)),d},getPages:function(a){var b=[],c=Math.floor(this.options.visiblePages/2),d=a-c+1-this.options.visiblePages%2,e=a+c;d<=0&&(d=1,e=this.options.visiblePages),e>this.options.totalPages&&(d=this.options.totalPages-this.options.visiblePages+1,e=this.options.totalPages);for(var f=d;f<=e;)b.push(f),f++;return{currentPage:a,numeric:b}},render:function(b){var c=this;this.$listContainer.children().remove();var d=this.buildListItems(b);a.each(d,function(a,b){c.$listContainer.append(b)}),this.$listContainer.children().each(function(){var d=a(this),e=d.data("page-type");switch(e){case"page":d.data("page")===b.currentPage&&d.addClass(c.options.activeClass);break;case"first":d.toggleClass(c.options.disabledClass,1===b.currentPage);break;case"last":d.toggleClass(c.options.disabledClass,b.currentPage===c.options.totalPages);break;case"prev":d.toggleClass(c.options.disabledClass,!c.options.loop&&1===b.currentPage);break;case"next":d.toggleClass(c.options.disabledClass,!c.options.loop&&b.currentPage===c.options.totalPages)}})},setupEvents:function(){var b=this;this.$listContainer.off("click").on("click","li",function(c){var d=a(this);return!d.hasClass(b.options.disabledClass)&&!d.hasClass(b.options.activeClass)&&(!b.options.href&&c.preventDefault(),void b.show(parseInt(d.data("page"))))})},makeHref:function(a){return this.options.href?this.generateQueryString(a):"#"},makeText:function(a,b){return a.replace(this.options.pageVariable,b).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(a){var b=this.getSearchString(a),c=new RegExp(this.options.pageVariable+"(=([^&#]*)|&|#|$)"),d=c.exec(b);return d&&d[2]?(d=decodeURIComponent(d[2]),d=parseInt(d),isNaN(d)?null:d):null},generateQueryString:function(a,b){var c=this.getSearchString(b),d=new RegExp(this.options.pageVariable+"=*[^&#]*");return c?"?"+c.replace(d,this.options.pageVariable+"="+a):""},getSearchString:function(a){var c=a||b.location.search;return""===c?null:(0===c.indexOf("?")&&(c=c.substr(1)),c)},getCurrentPage:function(){return this.currentPage}},a.fn.twbsPagination=function(b){var c,e=Array.prototype.slice.call(arguments,1),g=a(this),h=g.data("twbs-pagination"),i="object"==typeof b?b:{};return h||g.data("twbs-pagination",h=new f(this,i)),"string"==typeof b&&(c=h[b].apply(h,e)),c===d?g:c},a.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},a.fn.twbsPagination.Constructor=f,a.fn.twbsPagination.noConflict=function(){return a.fn.twbsPagination=e,this},a.fn.twbsPagination.version="1.4.1"}(window.jQuery,window,document); --------------------------------------------------------------------------------