├── .dockerignore ├── .gitignore ├── hooks └── build ├── LICENSE ├── cli ├── src │ ├── main │ │ ├── resources │ │ │ ├── colander.sh │ │ │ ├── colander.bat │ │ │ └── logback.xml │ │ ├── java │ │ │ └── info │ │ │ │ └── schnatterer │ │ │ │ └── colander │ │ │ │ └── cli │ │ │ │ ├── ExitStatus.java │ │ │ │ ├── ArgumentsParser.java │ │ │ │ ├── ColanderCli.java │ │ │ │ └── Arguments.java │ │ └── assembly │ │ │ └── assembly.xml │ └── test │ │ └── java │ │ └── info │ │ └── schnatterer │ │ └── colander │ │ └── cli │ │ ├── ColanderCliITCase.java │ │ ├── ColanderCliTest.java │ │ └── ArgumentsParserTest.java └── pom.xml ├── test-lib ├── src │ ├── main │ │ ├── resources │ │ │ ├── ColanderIT-expected.ics │ │ │ └── ColanderIT.ics │ │ └── java │ │ │ └── info │ │ │ └── schnatterer │ │ │ └── colander │ │ │ └── test │ │ │ └── ITCases.java │ └── test │ │ └── java │ │ └── info │ │ └── schnatterer │ │ └── colander │ │ └── test │ │ └── ITCasesTest.java └── pom.xml ├── .editorconfig ├── core ├── src │ ├── test │ │ ├── resources │ │ │ └── logback-test.xml │ │ └── java │ │ │ └── info │ │ │ └── schnatterer │ │ │ └── colander │ │ │ ├── ColanderITCase.java │ │ │ ├── RemoveFilterTest.java │ │ │ ├── RemoveEmptyEventFilterTest.java │ │ │ ├── ReplaceFilterTest.java │ │ │ ├── RemoveDuplicateEventFilterTest.java │ │ │ ├── FilterChainTest.java │ │ │ ├── ColanderIOTest.java │ │ │ └── ColanderTest.java │ └── main │ │ └── java │ │ └── info │ │ └── schnatterer │ │ └── colander │ │ ├── ColanderFilter.java │ │ ├── RemoveEmptyEventFilter.java │ │ ├── ColanderParserException.java │ │ ├── RemoveFilter.java │ │ ├── TypedColanderFilter.java │ │ ├── ReplaceFilter.java │ │ ├── FilterChain.java │ │ ├── RemoveDuplicateEventFilter.java │ │ ├── ColanderIO.java │ │ └── Colander.java └── pom.xml ├── .travis.yml ├── commons-lib ├── pom.xml └── src │ ├── main │ └── java │ │ └── info │ │ └── schnatterer │ │ └── colander │ │ └── Properties.java │ └── test │ └── java │ └── info │ └── schnatterer │ └── colander │ └── PropertiesTest.java ├── Dockerfile ├── README.md ├── Jenkinsfile └── pom.xml /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | 3 | !cli 4 | !commons-lib 5 | !core 6 | !test-lib 7 | !pom.xml 8 | # The git revision is used as part of the version name during the build 9 | !.git 10 | **/target 11 | **/*.iml 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | 12 | # Intellij 13 | .idea/ 14 | *.iml 15 | *.iws 16 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | POTENTIAL_TAG=$(git name-rev --name-only --tags HEAD) 5 | ADDITIONAL_BUILD_ARG="" 6 | if [ "${POTENTIAL_TAG}" != "undefined" ]; then 7 | GIT_TAG="${POTENTIAL_TAG}" 8 | ADDITIONAL_BUILD_ARG="-DperformRelease" 9 | fi 10 | 11 | docker image build \ 12 | --build-arg VCS_REF="${GIT_SHA1}" \ 13 | --build-arg SOURCE_REPOSITORY_URL="${SOURCE_REPOSITORY_URL}" \ 14 | --build-arg GIT_TAG="${GIT_TAG}" \ 15 | --build-arg BUILD_DATE="$(date --rfc-3339 ns)" \ 16 | --build-arg ADDITIONAL_BUILD_ARG="${ADDITIONAL_BUILD_ARG}" \ 17 | --tag ${IMAGE_NAME} \ 18 | . 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Johannes Schnatterer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/src/main/resources/colander.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2017 Johannes Schnatterer 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | 26 | BASEDIR=$(dirname $0) 27 | java -jar $BASEDIR/colander-cli.jar "$@" 28 | 29 | -------------------------------------------------------------------------------- /cli/src/main/resources/colander.bat: -------------------------------------------------------------------------------- 1 | @REM 2 | @REM The MIT License (MIT) 3 | @REM 4 | @REM Copyright (c) 2017 Johannes Schnatterer 5 | @REM 6 | @REM Permission is hereby granted, free of charge, to any person obtaining a copy 7 | @REM of this software and associated documentation files (the "Software"), to deal 8 | @REM in the Software without restriction, including without limitation the rights 9 | @REM to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | @REM copies of the Software, and to permit persons to whom the Software is 11 | @REM furnished to do so, subject to the following conditions: 12 | @REM 13 | @REM The above copyright notice and this permission notice shall be included in all 14 | @REM copies or substantial portions of the Software. 15 | @REM 16 | @REM THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | @REM IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | @REM FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | @REM AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | @REM LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | @REM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | @REM SOFTWARE. 23 | @REM 24 | 25 | @echo off 26 | java -jar %~dp0/colander-cli.jar %* 27 | -------------------------------------------------------------------------------- /test-lib/src/main/resources/ColanderIT-expected.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Berlin 6 | BEGIN:DAYLIGHT 7 | TZOFFSETFROM:+0100 8 | TZOFFSETTO:+0200 9 | TZNAME:CEST 10 | DTSTART:19700329T020000 11 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 12 | END:DAYLIGHT 13 | BEGIN:STANDARD 14 | TZOFFSETFROM:+0200 15 | TZOFFSETTO:+0100 16 | TZNAME:CET 17 | DTSTART:19701025T030000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 19 | END:STANDARD 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | CREATED:20170129T140245Z 23 | LAST-MODIFIED:20170129T140249Z 24 | DTSTAMP:20170129T140249Z 25 | UID:cbd6d085-9ff0-4f68-b733-1cffc5c99fff 26 | SUMMARY:Duplicate 27 | DTSTART;TZID=Europe/Berlin:20170102T160000 28 | DTEND;TZID=Europe/Berlin:20170102T170000 29 | TRANSP:OPAQUE 30 | CLASS:PUBLIC 31 | END:VEVENT 32 | BEGIN:VEVENT 33 | CREATED:20170129T140325Z 34 | LAST-MODIFIED:20170129T140353Z 35 | DTSTAMP:20170129T140353Z 36 | UID:5574e5d7-ea92-4dd2-b5f7-4fc10dc328bb 37 | SUMMARY:event Replace! 38 | DTSTART;TZID=Europe/Berlin:20170105T160000 39 | DTEND;TZID=Europe/Berlin:20170105T170000 40 | TRANSP:OPAQUE 41 | DESCRIPTION:FirstLine\nSecondLine\nThirdLine\n 42 | BEGIN:VALARM 43 | ACTION:DISPLAY 44 | TRIGGER;VALUE=DURATION:-PT15M 45 | END:VALARM 46 | CLASS:PUBLIC 47 | END:VEVENT 48 | BEGIN:VTODO 49 | SUMMARY:TDO Replace! 50 | DESCRIPTION: Some tdo 51 | STATUS:NEEDS ACTION 52 | PRIORITY:9 53 | LAST-MODIFIED:20170111T004843Z 54 | END:VTODO 55 | END:VCALENDAR 56 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2017 Johannes Schnatterer 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 all 14 | # 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 THE 22 | # SOFTWARE. 23 | # 24 | 25 | root = true 26 | 27 | [*] 28 | end_of_line = lf 29 | insert_final_newline = true 30 | charset = utf-8 31 | indent_style = space 32 | indent_size = 4 33 | trim_trailing_whitespace = true 34 | tab_width = 4 35 | 36 | [Makefile] 37 | indent_style = tab 38 | tab_width = 8 39 | 40 | # java properties must be saved in latin1 41 | [*.properties] 42 | charset = latin1 43 | -------------------------------------------------------------------------------- /core/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | %d{ISO8601} [%thread] %-5level %logger - %m%n 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /cli/src/main/java/info/schnatterer/colander/cli/ExitStatus.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | /** 27 | * Exit status of Colander CLI. 28 | */ 29 | enum ExitStatus { 30 | /** Successfully parsed calendar and wrote result. */ 31 | SUCCESS(0), 32 | /** Invalid command line arguments. */ 33 | ERROR_ARGS(1), 34 | /** Error parsing input ICS or writing output. */ 35 | ERROR_PARSING(2); 36 | 37 | final int numericStatus; 38 | 39 | ExitStatus(int numericStatus) { 40 | this.numericStatus = numericStatus; 41 | } 42 | 43 | public int getExitStatus() { 44 | return numericStatus; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk11 4 | 5 | addons: 6 | sonarqube: 7 | organization: "schnatterer-github" 8 | token: 9 | secure: "WfnvLKNi/FVkI6QGD86eWWok29zpy5mrCibfxhzqR/oGVv7KLXV0qtUBpZndqP6g9+CaXyDT5/7wbXNi+wcC9Gr3IHjJg8pxAa4LjD5Ch9l/mmGXwi+N62fxmrGndUr3LaL7VjduzTAcM2huICcKVQiM0U382p8N6W0gUojVc3+P/Wu+BxDMWvVPgEcO4QD3XqJf+2RvIQjolwWc/kCQRcn08ou/UVi/6hBvbXF2qYG/B4bW4ZuQ2rq6ZdVIa25uogvzeacc8HUXTB+ZbSIcdTnUKw0j0R3aDqqxcHk1i/b++P/3QNEIrC0OYOvJslkCHCH14dbLWuS7GSfChTxtk0p3L7vK5mPZ5fq+z11RqTEk0HkaeKDLh9BSBNiIdKyoeul8TO2We30yNd9CweQnXxTEt0klYU+GQE8uk9bs2ceUYZp/mc95suCRCfnJDsL5EkE+LhVK6AZ/dW0Q7hj//Jd8HVXX/WCevedydI21o7FZecWJXJtspctlsg+loYXZVzDQcaNGZIpFzr5ULjOSDdRwz+NV5fXWVfzI0vW0LvIFicjaRbyTBQ1Vp8xtF4T8qmVoAGK0Re7J8sBQIubqzRmYxLG+SG+Uye0ru2EXztJyHjR00Sh0DNbcD59GPJCWUrzx9JxH5Khp6SMY6Nt0Y2Po9gL+XDW3qmsMz25oSwY=" 10 | github_token: 11 | secure: "oTzSRrGgInUETLqQG2diTm1tZBaK6FmdZspGuSfqCtVGIVp9qLqoVmduwf8jNt2R6P2VWNVqSyX4DfyJOUugaygVShrZar+pil1eK/vPMQtbIIJ1Ga6/Jb+MvNTEhBgZ1lVdzY6vfLGLFLpS48BHOPn58aSbCjFlWBvRyXqq/+CA5Fm/7szxJWSbh4bim9yP9ek5JGQ6XcoGIJ4KQKcGBIFLge+1qd4h3kQNxfd1y8hN5F+r+O82o7yiM4ui40qubxfojDNTQRXPxbbJugPFrtytTZy6Htannm2VSvK05Wr83h3KvLpRxK/QRaHZU3CPPZ0OC7MMXH7ZoY3yflgDZs6p2ZL8KCjsKxZYuEM/j0NSkq8m414RRKgW+wr138jzuG/7zv8FYQDE3YV7r12TFodDycUEZU4GTJOvIlQHiBFVbWLjnw8dTldU3XWEt15rIjZYfhanJULOXDEJyc9vZvbJq5tpL5JKJFj/mveWDlTsiLZk3YP6isAHpln5DDqd9+7zbYDQEi3o+CMopr3mbeWPIdtzqYy/I/Gf35hBw2bWfTZESL89J96UE+J6hwgYDTFV+LsC0kB6P1W4jtZtKNxgwQo5FAYm4Fg/XcA9AcHeY9dOwisgUsvLA1Tj5Luj0VljA0cZmsGHAncOSf1HQgx0o+gK//Uu+dG1LjR7ieo=" 12 | branches: 13 | - master 14 | - develop 15 | 16 | script: 17 | - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent install sonar:sonar -Dsonar.branch= 18 | - chmod +x hooks/build 19 | - IMAGE_NAME=colander hooks/build 20 | # Do a very simple smoke test 21 | - docker run --rm colander | grep -help 22 | cache: 23 | directories: 24 | - '$HOME/.m2/repository' 25 | - '$HOME/.sonar/cache' 26 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/ColanderFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.component.CalendarComponent; 27 | 28 | import java.util.Optional; 29 | 30 | /** 31 | * Interface for filters that mutate or delete calender componennts (such as events, ToDos, etc.) in a filter chain. 32 | */ 33 | @FunctionalInterface 34 | public interface ColanderFilter { 35 | /** 36 | * Filters a calendar component. 37 | * 38 | * @param component subject to be filtered. Never {@code null}! 39 | * @return a calendar component to be passed to next filter or {@link Optional#empty()} if the component should be 40 | * removed. 41 | * @throws ColanderParserException if anything goes wrong 42 | */ 43 | Optional apply(CalendarComponent component); 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/RemoveEmptyEventFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.component.CalendarComponent; 27 | import net.fortuna.ical4j.model.component.VEvent; 28 | 29 | import java.util.Optional; 30 | 31 | /** 32 | * Removes event when it has 33 | * 37 | */ 38 | public class RemoveEmptyEventFilter extends TypedColanderFilter { 39 | 40 | @Override 41 | protected Optional applyTyped(VEvent event) { 42 | if (Properties.getSummaryValue(event).orElse("").isEmpty() && 43 | Properties.getDescriptionValue(event).orElse("").isEmpty()) { 44 | return Optional.empty(); 45 | } else { 46 | return Optional.of(event); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test-lib/src/test/java/info/schnatterer/colander/test/ITCasesTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.test; 25 | 26 | import org.junit.ClassRule; 27 | import org.junit.Test; 28 | import org.junit.rules.TemporaryFolder; 29 | 30 | import static org.hamcrest.Matchers.containsString; 31 | import static org.junit.Assert.assertThat; 32 | 33 | public class ITCasesTest { 34 | private static final String ICS_FILE_EXPECTED = "ColanderIT-expected.ics"; 35 | 36 | @ClassRule 37 | public static TemporaryFolder folder = new TemporaryFolder(); 38 | 39 | @Test 40 | public void verifyParsedIcs() throws Exception { 41 | // No exception means success 42 | ITCases.verifyParsedIcs(ITCases.getFilePathTestIcs(folder), 43 | ITCases.getFilePathTestIcs(ICS_FILE_EXPECTED, folder)); 44 | } 45 | 46 | @Test 47 | public void getFilePathTestIcs() throws Exception { 48 | // Assert Written in .tmp file 49 | assertThat(ITCases.getFilePathTestIcs(folder), containsString("tmp")); 50 | } 51 | 52 | @Test(expected = AssertionError.class) 53 | public void getFilePathTestIcsNotPresent() throws Exception { 54 | ITCases.getFilePathTestIcs("Not exists", folder); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /commons-lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 30 | 4.0.0 31 | 32 | 33 | info.schnatterer.colander 34 | colander-parent 35 | 0.2.1-SNAPSHOT 36 | 37 | 38 | colander-commons-lib 39 | commons-lib 40 | 41 | jar 42 | 43 | 44 | ${project.parent.basedir} 45 | 46 | 47 | 48 | 49 | 50 | org.slf4j 51 | slf4j-api 52 | ${slf4j.version} 53 | 54 | 55 | 56 | 57 | org.mnode.ical4j 58 | ical4j 59 | 2.0.2 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /cli/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | logs/colander-${bySecond}.log 35 | 36 | %d{ISO8601} [%thread] %-5level %logger - %m%n 37 | 38 | 39 | 40 | 41 | 42 | 43 | INFO 44 | 45 | 46 | 47 | %m%n%nopex 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/ColanderParserException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | /** 27 | * When parsing of a calendar failed. 28 | */ 29 | public class ColanderParserException extends RuntimeException { 30 | 31 | /** 32 | * Constructs a new parser exception with the specified cause and a detail message of 33 | * (cause==null ? null : cause.toString()) (which typically contains the class and detail message of 34 | * cause). This constructor is useful for runtime exceptions that are little more than wrappers for other 35 | * throwables. 36 | * 37 | * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 38 | * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) 39 | */ 40 | ColanderParserException(Throwable cause) { 41 | // Don't class name of cause in message, as we don't want to see it on the CLI. 42 | super(cause.getMessage(), cause); 43 | } 44 | 45 | /** 46 | * Constructs a new parser exception with the specified detail message. The cause is not initialized, and may 47 | * subsequently be initialized by a call to {@link #initCause}. 48 | * 49 | * @param message the detail message. The detail message is saved for later retrieval by the 50 | * {@link #getMessage()} method. 51 | */ 52 | ColanderParserException(String message) { 53 | super(message); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /commons-lib/src/main/java/info/schnatterer/colander/Properties.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Property; 27 | import net.fortuna.ical4j.model.component.CalendarComponent; 28 | 29 | import java.util.Optional; 30 | 31 | /** 32 | * Conveniently provides {@link Property}s in a {@code null}-safe way. 33 | */ 34 | public class Properties { 35 | 36 | public static Optional getSummary(CalendarComponent component) { 37 | return getProperty(component, Property.SUMMARY); 38 | } 39 | 40 | public static Optional getSummaryValue(CalendarComponent component) { 41 | return getPropertyValue(component, Property.SUMMARY); 42 | } 43 | 44 | public static Optional getDescription(CalendarComponent component) { 45 | return getProperty(component, Property.DESCRIPTION); 46 | } 47 | 48 | public static Optional getDescriptionValue(CalendarComponent component) { 49 | return getPropertyValue(component, Property.DESCRIPTION); 50 | } 51 | 52 | public static Optional getProperty(CalendarComponent component, String propertyName) { 53 | return Optional.ofNullable(component.getProperty(propertyName)); 54 | } 55 | 56 | public static Optional getPropertyValue(CalendarComponent component, String propertyName) { 57 | return getProperty(component, propertyName) 58 | .map(property -> Optional.ofNullable(property.getValue())) 59 | .orElse(Optional.empty()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/RemoveFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Property; 27 | import net.fortuna.ical4j.model.component.CalendarComponent; 28 | 29 | import java.util.Optional; 30 | 31 | /** 32 | * Removes calender component, when one of its properties contains a specific string. 33 | */ 34 | public class RemoveFilter implements ColanderFilter { 35 | private String propertyContainsString; 36 | private final String propertyName; 37 | 38 | /** 39 | * @param propertyContainsString remove component when it's property contains this string 40 | * @param propertyName the event property to search 41 | */ 42 | public RemoveFilter(String propertyContainsString, String propertyName) { 43 | this.propertyContainsString = propertyContainsString; 44 | this.propertyName = propertyName; 45 | } 46 | 47 | @Override 48 | public Optional apply(CalendarComponent component) { 49 | if (contains(component.getProperty(propertyName))) { 50 | return Optional.empty(); 51 | } else { 52 | return Optional.of(component); 53 | } 54 | } 55 | 56 | private boolean contains(Property property) { 57 | return !(property == null || property.getValue() == null) && property.getValue().contains(propertyContainsString); 58 | } 59 | 60 | public String getPropertyContainsString() { 61 | return propertyContainsString; 62 | } 63 | 64 | public String getPropertyName() { return propertyName; } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/ColanderITCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import info.schnatterer.colander.test.ITCases; 27 | import net.fortuna.ical4j.model.Property; 28 | import org.junit.Rule; 29 | import org.junit.Test; 30 | import org.junit.rules.TemporaryFolder; 31 | 32 | import java.io.File; 33 | import java.util.Optional; 34 | 35 | import static org.junit.Assert.assertTrue; 36 | 37 | /** 38 | * Integration tests that tests colander end-to-end, from ics file to ics file. 39 | */ 40 | public class ColanderITCase { 41 | 42 | @Rule 43 | public TemporaryFolder folder = new TemporaryFolder(); 44 | 45 | @Test 46 | public void endToEnd() throws Exception { 47 | String outputPath = folder.getRoot().toString() + "/out.ics"; 48 | String inputPath = ITCases.getFilePathTestIcs(folder); 49 | Colander.toss(inputPath) 50 | .removeDuplicateEvents() 51 | .removeEmptyEvents() 52 | .removePropertyContains(Property.SUMMARY, "Remove me") 53 | .removeDescriptionContains("Remove me 2") 54 | // Generic replace in property 55 | .replaceInProperty(Property.DESCRIPTION, "L.ne", "Line") 56 | // Convenience: replace in property summary 57 | .replaceInSummary("Replace", "Replace!") 58 | // NOP filter 59 | .filter(Optional::of) 60 | .rinse() 61 | .toFile(outputPath); 62 | assertTrue("Output not written", new File(outputPath).exists()); 63 | ITCases.verifyParsedIcs(inputPath, outputPath); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 30 | 4.0.0 31 | 32 | 33 | info.schnatterer.colander 34 | colander-parent 35 | 0.2.1-SNAPSHOT 36 | 37 | 38 | colander-core 39 | core 40 | 41 | jar 42 | 43 | 44 | ${project.parent.basedir} 45 | 46 | 47 | 48 | 49 | 50 | ${project.parent.groupId} 51 | colander-commons-lib 52 | ${project.parent.version} 53 | 54 | 55 | 56 | 57 | ${project.parent.groupId} 58 | colander-test-lib 59 | ${project.parent.version} 60 | test 61 | 62 | 63 | 64 | ch.qos.logback 65 | logback-classic 66 | ${logback.version} 67 | test 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /cli/src/main/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 26 | 27 | bin 28 | 29 | zip 30 | 31 | 32 | 33 | 34 | false 35 | lib 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | ${main.basedir} 44 | / 45 | 46 | README* 47 | LICENSE* 48 | NOTICE* 49 | 50 | 51 | 52 | 53 | ${project.build.directory} 54 | / 55 | 56 | *.jar 57 | 58 | 59 | *javadoc.jar 60 | *sources.jar 61 | 62 | 63 | 64 | 65 | ${project.basedir}/src/main/resources/ 66 | / 67 | 68 | *.bat 69 | *.sh 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/RemoveFilterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Date; 27 | import net.fortuna.ical4j.model.Property; 28 | import net.fortuna.ical4j.model.component.VEvent; 29 | import net.fortuna.ical4j.model.property.Summary; 30 | import org.junit.Test; 31 | 32 | import static org.assertj.core.api.Assertions.assertThat; 33 | 34 | public class RemoveFilterTest { 35 | 36 | @Test 37 | public void applyMatch() throws Exception { 38 | RemoveFilter filter = new RemoveFilter("hallo", Property.SUMMARY); 39 | VEvent event = new VEvent(new Date(), "hallo icaltools"); 40 | assertThat(filter.apply(event)).isEmpty(); 41 | } 42 | 43 | @Test 44 | public void applyNoMatch() throws Exception { 45 | RemoveFilter filter = new RemoveFilter("hallo", Property.SUMMARY); 46 | VEvent event = new VEvent(new Date(), "hullo icaltools"); 47 | assertThat(filter.apply(event)).hasValueSatisfying(actual -> assertThat(actual).isSameAs(event)); 48 | } 49 | 50 | @Test 51 | public void filterSummaryDoesNotExist() throws Exception { 52 | RemoveFilter filter = new RemoveFilter("hallo", Property.SUMMARY); 53 | VEvent event = new VEvent(); 54 | assertThat(filter.apply(event)).hasValueSatisfying(actual -> assertThat(actual).isSameAs(event)); 55 | } 56 | 57 | @Test 58 | public void filterSummaryDoesHaveValue() throws Exception { 59 | RemoveFilter filter = new RemoveFilter("hallo", Property.SUMMARY); 60 | VEvent event = new VEvent(); 61 | event.getProperties().add(new Summary(null)); 62 | assertThat(filter.apply(event)).hasValueSatisfying(actual -> assertThat(actual).isSameAs(event)); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /test-lib/src/main/resources/ColanderIT.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Berlin 6 | BEGIN:DAYLIGHT 7 | TZOFFSETFROM:+0100 8 | TZOFFSETTO:+0200 9 | TZNAME:CEST 10 | DTSTART:19700329T020000 11 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 12 | END:DAYLIGHT 13 | BEGIN:STANDARD 14 | TZOFFSETFROM:+0200 15 | TZOFFSETTO:+0100 16 | TZNAME:CET 17 | DTSTART:19701025T030000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 19 | END:STANDARD 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | CREATED:20170129T140245Z 23 | LAST-MODIFIED:20170129T140249Z 24 | DTSTAMP:20170129T140249Z 25 | UID:cbd6d085-9ff0-4f68-b733-1cffc5c99fff 26 | SUMMARY:Duplicate 27 | DTSTART;TZID=Europe/Berlin:20170102T160000 28 | DTEND;TZID=Europe/Berlin:20170102T170000 29 | TRANSP:OPAQUE 30 | CLASS:PUBLIC 31 | END:VEVENT 32 | BEGIN:VEVENT 33 | CREATED:20170129T140251Z 34 | LAST-MODIFIED:20170129T140255Z 35 | DTSTAMP:20170129T140255Z 36 | UID:b0c4cf3e-d2fb-4ae5-822c-60ee614e8ffc 37 | SUMMARY:Duplicate 38 | DTSTART;TZID=Europe/Berlin:20170102T160000 39 | DTEND;TZID=Europe/Berlin:20170102T170000 40 | TRANSP:OPAQUE 41 | CLASS:PUBLIC 42 | END:VEVENT 43 | BEGIN:VEVENT 44 | CREATED:20170129T140301Z 45 | LAST-MODIFIED:20170129T140308Z 46 | DTSTAMP:20170129T140308Z 47 | UID:1756e0f0-5194-467a-aa14-a3b4e3049cd4 48 | SUMMARY:Remove me 49 | DTSTART;TZID=Europe/Berlin:20170103T160000 50 | DTEND;TZID=Europe/Berlin:20170103T170000 51 | TRANSP:OPAQUE 52 | CLASS:PUBLIC 53 | END:VEVENT 54 | BEGIN:VEVENT 55 | CREATED:20170129T140301Z 56 | LAST-MODIFIED:20170129T140308Z 57 | DTSTAMP:20170129T140308Z 58 | UID:1756e0f0-5194-467a-aa14-a3b4e3049cd4 59 | SUMMARY:Smry 60 | DESCRIPTION: Remove me 2 61 | DTSTART;TZID=Europe/Berlin:20170103T160000 62 | DTEND;TZID=Europe/Berlin:20170103T170000 63 | TRANSP:OPAQUE 64 | CLASS:PUBLIC 65 | END:VEVENT 66 | BEGIN:VEVENT 67 | CREATED:20170129T140315Z 68 | LAST-MODIFIED:20170129T140318Z 69 | DTSTAMP:20170129T140318Z 70 | UID:8d702a68-c586-4904-98b7-dc4877b9ff3c 71 | DTSTART;TZID=Europe/Berlin:20170104T160000 72 | DTEND;TZID=Europe/Berlin:20170104T170000 73 | TRANSP:OPAQUE 74 | CLASS:PUBLIC 75 | END:VEVENT 76 | BEGIN:VEVENT 77 | CREATED:20170129T140325Z 78 | LAST-MODIFIED:20170129T140353Z 79 | DTSTAMP:20170129T140353Z 80 | UID:5574e5d7-ea92-4dd2-b5f7-4fc10dc328bb 81 | SUMMARY:event Replace 82 | DTSTART;TZID=Europe/Berlin:20170105T160000 83 | DTEND;TZID=Europe/Berlin:20170105T170000 84 | TRANSP:OPAQUE 85 | DESCRIPTION:FirstLone\nSecondLane\nThirdLune\n 86 | BEGIN:VALARM 87 | ACTION:DISPLAY 88 | TRIGGER;VALUE=DURATION:-PT15M 89 | END:VALARM 90 | CLASS:PUBLIC 91 | END:VEVENT 92 | BEGIN:VTODO 93 | SUMMARY:TDO Replace 94 | DESCRIPTION: Some tdo 95 | STATUS:NEEDS ACTION 96 | PRIORITY:9 97 | LAST-MODIFIED:20170111T004843Z 98 | END:VTODO 99 | BEGIN:VTODO 100 | SUMMARY:Remove me 101 | DESCRIPTION:This is going to be removed 102 | PRIORITY:5 103 | LAST-MODIFIED:20170201T004843Z 104 | END:VTODO 105 | BEGIN:VTODO 106 | SUMMARY:smry 107 | DESCRIPTION:Remove me 2 108 | PRIORITY:5 109 | LAST-MODIFIED:20170201T004843Z 110 | END:VTODO 111 | END:VCALENDAR 112 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/TypedColanderFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.component.CalendarComponent; 27 | 28 | import java.lang.reflect.ParameterizedType; 29 | import java.util.Optional; 30 | 31 | /** 32 | * Base class for filters, that filter only specific 33 | */ 34 | public abstract class TypedColanderFilter implements ColanderFilter { 35 | 36 | 37 | /** 38 | * Template method for concrete classes. Same as {@link #apply(CalendarComponent)}, but casted to the calender 39 | * component type that this filter applies to. 40 | * 41 | * @param concreteComponent concrete calender component type that this filter applies to. 42 | * @return a calendar component to be passed to next filter or {@link Optional#empty()} if the component should be 43 | * removed. 44 | * @throws ColanderParserException if anything goes wrong 45 | */ 46 | protected abstract Optional applyTyped(T concreteComponent); 47 | 48 | 49 | @Override 50 | public Optional apply(CalendarComponent abstractComponent) { 51 | Class filteredComponentType = getFilteredComponentType(); 52 | if (filteredComponentType.isInstance(abstractComponent)) { 53 | return applyTyped(filteredComponentType.cast(abstractComponent)); 54 | } else { 55 | // Don't filter 56 | return Optional.of(abstractComponent); 57 | } 58 | } 59 | 60 | 61 | @SuppressWarnings("unchecked") // Due to type erasure, there seem to be no good options of getting the generic type. 62 | private Class getFilteredComponentType() { 63 | // Note that this only works for complex type hierarchies. Let's just not create these! 64 | return (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/ReplaceFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Property; 27 | import net.fortuna.ical4j.model.component.CalendarComponent; 28 | 29 | import java.io.IOException; 30 | import java.net.URISyntaxException; 31 | import java.text.ParseException; 32 | import java.util.Optional; 33 | 34 | /** 35 | * Replaces regex in a {@link Property} of a calender component. 36 | */ 37 | public class ReplaceFilter implements ColanderFilter { 38 | 39 | private final String stringToReplace; 40 | private final String regex; 41 | private final String propertyName; 42 | 43 | /** 44 | * @param regex regex to match 45 | * @param stringToReplace regex to replace matching regex 46 | * @param propertyName the event property to replace 47 | */ 48 | public ReplaceFilter(String regex, String stringToReplace, String propertyName) { 49 | this.regex = regex; 50 | this.stringToReplace = stringToReplace; 51 | this.propertyName = propertyName; 52 | } 53 | 54 | @Override 55 | public Optional apply(CalendarComponent component) { 56 | try { 57 | replace(component.getProperty(propertyName)); 58 | } catch (IOException | URISyntaxException | ParseException e) { 59 | throw new ColanderParserException(e); 60 | } 61 | return Optional.of(component); 62 | } 63 | 64 | /** 65 | * Visible for testing. 66 | */ 67 | void replace(Property property) throws IOException, URISyntaxException, ParseException { 68 | if (property == null) { 69 | return; 70 | } 71 | String value = property.getValue(); 72 | if (value != null) { 73 | property.setValue(value.replaceAll(regex, stringToReplace)); 74 | } 75 | } 76 | 77 | public String getRegex() { return regex; } 78 | 79 | public String getStringToReplace() { return stringToReplace; } 80 | 81 | public String getPropertyName() { return propertyName; } 82 | } 83 | -------------------------------------------------------------------------------- /cli/src/test/java/info/schnatterer/colander/cli/ColanderCliITCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | import info.schnatterer.colander.test.ITCases; 27 | import org.junit.Rule; 28 | import org.junit.Test; 29 | import org.junit.contrib.java.lang.system.ExpectedSystemExit; 30 | import org.junit.rules.TemporaryFolder; 31 | 32 | import java.io.File; 33 | 34 | import static org.junit.Assert.assertTrue; 35 | 36 | /** 37 | * Integration tests that tests colander CLI end-to-end, from ics file to ics file, using 38 | * {@link ColanderCli#main(String[])} method and exit codes. 39 | */ 40 | public class ColanderCliITCase { 41 | 42 | @Rule 43 | public TemporaryFolder folder = new TemporaryFolder(); 44 | 45 | @Rule 46 | public final ExpectedSystemExit exit = ExpectedSystemExit.none(); 47 | 48 | @Test 49 | public void endToEnd() throws Exception { 50 | String inputPath = ITCases.getFilePathTestIcs(folder); 51 | String outputPath = folder.getRoot().toString() + "/out.ics"; 52 | exit.expectSystemExitWithStatus(0); 53 | exit.checkAssertionAfterwards(() -> { 54 | assertTrue("Output not written", new File(outputPath).exists()); 55 | ITCases.verifyParsedIcs(inputPath, outputPath); 56 | }); 57 | execute( 58 | "--remove-duplicate-events", 59 | "--remove-empty-events", 60 | "--remove-summary", "Remove me", 61 | "--remove-description", "Remove me 2", 62 | "--replace-description L.ne=Line", 63 | "--replace-summary Replace=Replace!", 64 | inputPath, 65 | outputPath 66 | ); 67 | } 68 | 69 | @Test 70 | public void endToEndParsingArgs() throws Exception { 71 | exit.expectSystemExitWithStatus(1); 72 | execute("--wtf"); 73 | } 74 | 75 | @Test 76 | public void endToEndExceptionParsing() throws Exception { 77 | exit.expectSystemExitWithStatus(2); 78 | // Try to overwrite input file 79 | execute(ITCases.getFilePathTestIcs(folder), ITCases.getFilePathTestIcs(folder)); 80 | } 81 | 82 | private void execute(String... args) { ColanderCli.main(args); } 83 | } 84 | -------------------------------------------------------------------------------- /test-lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 30 | 4.0.0 31 | 32 | 33 | info.schnatterer.colander 34 | colander-parent 35 | 0.2.1-SNAPSHOT 36 | 37 | 38 | colander-test-lib 39 | test-lib 40 | 41 | jar 42 | 43 | 44 | ${project.parent.basedir} 45 | 46 | 47 | 48 | 49 | ${project.parent.groupId} 50 | colander-commons-lib 51 | ${project.parent.version} 52 | 53 | 54 | 55 | 56 | org.slf4j 57 | slf4j-api 58 | ${slf4j.version} 59 | 60 | 61 | 62 | 63 | junit 64 | junit 65 | compile 66 | 67 | 68 | org.hamcrest 69 | hamcrest-junit 70 | compile 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-failsafe-plugin 79 | 80 | 81 | **/ITCasesTest.java 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2017 Johannes Schnatterer 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 all 14 | # 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 THE 22 | # SOFTWARE. 23 | # 24 | 25 | # Define maven version for all stages 26 | # Contains git, in order to be able to write version info during maven build 27 | FROM maven:3.6.1-jdk-11 as maven-git 28 | 29 | FROM maven-git as mavencache 30 | ENV MAVEN_OPTS=-Dmaven.repo.local=/mvn 31 | COPY pom.xml /mvn/ 32 | COPY cli/pom.xml /mvn/cli/ 33 | COPY commons-lib/pom.xml /mvn/commons-lib/ 34 | COPY core/pom.xml /mvn/core/ 35 | COPY test-lib/pom.xml /mvn/test-lib/ 36 | WORKDIR /mvn 37 | RUN mvn compile dependency:resolve dependency:resolve-plugins # --fail-never 38 | 39 | FROM maven-git as mavenbuild 40 | ARG ADDITIONAL_BUILD_ARG 41 | ENV MAVEN_OPTS=-Dmaven.repo.local=/mvn 42 | COPY . /mvn 43 | COPY --from=mavencache /mvn/ /mvn/ 44 | WORKDIR /mvn 45 | RUN set -x && mvn package -Djar ${ADDITIONAL_BUILD_ARG} 46 | RUN rm -rf /mvn/cli/target/colander-cli-*-sources.jar && \ 47 | rm -rf /mvn/cli/target/colander-cli-*-javadoc.jar 48 | RUN mv /mvn/cli/target/colander-cli-*.jar /colander.jar 49 | 50 | # Only way to make distroless build deterministic: Use repo digest 51 | # $ docker pull gcr.io/distroless/java:11 52 | # Digest: sha256:da8aa0fa074d0ed9c4b71ad15af5dffdf6afdd768efbe2f0f7b0d60829278630 53 | # $ docker run --rm -ti gcr.io/distroless/java:11 -version 54 | # openjdk version "11.0.2" 2019-01-15 55 | FROM gcr.io/distroless/java@sha256:da8aa0fa074d0ed9c4b71ad15af5dffdf6afdd768efbe2f0f7b0d60829278630 56 | ARG VCS_REF 57 | ARG SOURCE_REPOSITORY_URL 58 | ARG GIT_TAG 59 | ARG BUILD_DATE 60 | # See https://github.com/opencontainers/image-spec/blob/master/annotations.md 61 | LABEL org.opencontainers.image.created="${BUILD_DATE}" \ 62 | org.opencontainers.image.authors="schnatterer" \ 63 | org.opencontainers.image.url="https://hub.docker.com/r/schnatterer/colander/" \ 64 | org.opencontainers.image.documentation="https://hub.docker.com/r/schnatterer/colander/" \ 65 | org.opencontainers.image.source="${SOURCE_REPOSITORY_URL}" \ 66 | org.opencontainers.image.version="${GIT_TAG}" \ 67 | org.opencontainers.image.revision="${VCS_REF}" \ 68 | org.opencontainers.image.vendor="schnatterer" \ 69 | org.opencontainers.image.licenses="MIT" \ 70 | org.opencontainers.image.title="colander" \ 71 | org.opencontainers.image.description="colander - filtering your calendar" 72 | 73 | COPY --from=mavenbuild /colander.jar /app/colander.jar 74 | ENTRYPOINT ["java", "-jar", "/app/colander.jar"] 75 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/RemoveEmptyEventFilterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Date; 27 | import net.fortuna.ical4j.model.component.VEvent; 28 | import net.fortuna.ical4j.model.property.Description; 29 | import org.junit.Test; 30 | 31 | import static org.assertj.core.api.Assertions.assertThat; 32 | import static org.junit.Assert.assertSame; 33 | 34 | public class RemoveEmptyEventFilterTest { 35 | private RemoveEmptyEventFilter filter = new RemoveEmptyEventFilter(); 36 | 37 | @Test 38 | public void applyMatch() throws Exception { 39 | VEvent event = new VEvent(false); 40 | assertThat(filter.apply(event)).isEmpty(); 41 | } 42 | 43 | @Test 44 | public void applyMatchEmptyStrings() throws Exception { 45 | VEvent event = new VEvent(new Date(), ""); 46 | event.getProperties().add(new Description("")); 47 | assertThat(filter.apply(event)).isEmpty(); 48 | } 49 | 50 | @Test 51 | public void applyMatchNull() throws Exception { 52 | VEvent event = new VEvent(new Date(), null); 53 | event.getProperties().add(new Description(null)); 54 | assertThat(filter.apply(event)).isEmpty(); 55 | } 56 | 57 | @Test 58 | public void applyDescriptionEmpty() throws Exception { 59 | VEvent event = new VEvent(new Date(), "sumry"); 60 | assertSame("Unexpected filtering result", event, filter.apply(event).orElse(null)); 61 | } 62 | 63 | @Test 64 | public void applySummaryEmpty() throws Exception { 65 | VEvent event = new VEvent(false); 66 | event.getProperties().add(new Description("descr")); 67 | assertSame("Unexpected filtering result", event, filter.apply(event).orElse(null)); 68 | } 69 | 70 | @Test 71 | public void applySummaryNull() throws Exception { 72 | VEvent event = new VEvent(new Date(), null); 73 | event.getProperties().add(new Description("desc")); 74 | assertSame("Unexpected filtering result", event, filter.apply(event).orElse(null)); 75 | } 76 | 77 | @Test 78 | public void applyDescriptionNull() throws Exception { 79 | VEvent event = new VEvent(new Date(), "sumry"); 80 | event.getProperties().add(new Description(null)); 81 | assertSame("Unexpected filtering result", event, filter.apply(event).orElse(null)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /commons-lib/src/test/java/info/schnatterer/colander/PropertiesTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.component.CalendarComponent; 27 | import net.fortuna.ical4j.model.component.VEvent; 28 | import net.fortuna.ical4j.model.property.Description; 29 | import net.fortuna.ical4j.model.property.Summary; 30 | import org.junit.Assert; 31 | import org.junit.Test; 32 | 33 | import java.util.Optional; 34 | 35 | public class PropertiesTest { 36 | CalendarComponent calendarComponent = new VEvent(); 37 | 38 | @Test 39 | public void getProperty() throws Exception { 40 | Summary expectedSummary = new Summary("value"); 41 | calendarComponent.getProperties().add(expectedSummary); 42 | Assert.assertEquals(expectedSummary, Properties.getSummary(calendarComponent).orElse(null)); 43 | } 44 | 45 | @Test 46 | public void getPropertyNoSummary() throws Exception { 47 | Assert.assertEquals(Optional.empty(), Properties.getSummary(calendarComponent)); 48 | } 49 | 50 | @Test 51 | public void getPropertyValue() throws Exception { 52 | String expectedValue = "val"; 53 | calendarComponent.getProperties().add(new Summary(expectedValue)); 54 | Assert.assertEquals(expectedValue, Properties.getSummaryValue(calendarComponent).orElse(null)); 55 | } 56 | 57 | @Test 58 | public void getPropertyValueNoSummary() throws Exception { 59 | Assert.assertEquals(Optional.empty(), Properties.getSummaryValue(calendarComponent)); 60 | } 61 | 62 | @Test 63 | public void getPropertyValueNoSummaryValue() throws Exception { 64 | Summary expectedSummary = new Summary(null); 65 | calendarComponent.getProperties().add(expectedSummary); 66 | Assert.assertEquals(Optional.empty(), Properties.getSummaryValue(calendarComponent)); 67 | } 68 | 69 | @Test 70 | public void getDescription() throws Exception { 71 | Description expectedDescription = new Description("value"); 72 | calendarComponent.getProperties().add(expectedDescription); 73 | Assert.assertEquals(expectedDescription, Properties.getDescription(calendarComponent).orElse(null)); 74 | } 75 | 76 | @Test 77 | public void getDescriptionValue() throws Exception { 78 | String expectedValue = "val"; 79 | calendarComponent.getProperties().add(new Description(expectedValue)); 80 | Assert.assertEquals(expectedValue, Properties.getDescriptionValue(calendarComponent).orElse(null)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cli/src/main/java/info/schnatterer/colander/cli/ArgumentsParser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | import com.beust.jcommander.JCommander; 27 | import com.beust.jcommander.ParameterException; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | /** 32 | * Parser for CLI-arguments 33 | */ 34 | class ArgumentsParser { 35 | private static final Logger LOG = LoggerFactory.getLogger(ArgumentsParser.class); 36 | 37 | /** Use {@link #read(String[], String)} instead of constructor. */ 38 | ArgumentsParser() { 39 | } 40 | 41 | /** 42 | * Reads the command line parameters and prints error messages when something went wrong. 43 | * 44 | * @param argv arguments passed via CLI 45 | * @return an instance of {@link Arguments} 46 | * @throws ArgumentException on syntax error 47 | */ 48 | @SuppressWarnings("squid:S2629") // Log statements are used for console output 49 | public static Arguments read(String[] argv, String programName) { 50 | Arguments arguments = new Arguments(); 51 | JCommander commander = new JCommander(arguments); 52 | try { 53 | commander.setProgramName(programName); 54 | commander.parse(argv); 55 | } catch (ParameterException e) { 56 | // Print error and usage 57 | String usage = createUsage(e.getMessage() + System.lineSeparator(), commander); 58 | LOG.error(usage); 59 | // Rethrow, so the main application knows something went wrong 60 | throw new ArgumentException(usage, e); 61 | } 62 | 63 | if (arguments.isHelp()) { 64 | LOG.info(createUsage("", commander)); 65 | } 66 | 67 | return arguments; 68 | } 69 | 70 | /** 71 | * Creates a usage string to be displayed on console. 72 | * 73 | * @param prefix written before usage 74 | * @return the usage string 75 | */ 76 | private static String createUsage(String prefix, JCommander commander) { 77 | StringBuilder usage = new StringBuilder(prefix); 78 | commander.usage(usage, " "); 79 | return usage.toString(); 80 | } 81 | 82 | 83 | /** 84 | * Exception thrown when parameter syntax is invalid. 85 | */ 86 | static class ArgumentException extends RuntimeException { 87 | /** 88 | * Constructs a new runtime exception with the specified detail message and cause. 89 | *

Note that the detail message associated with {@code cause} is not automatically incorporated in 90 | * this runtime exception's detail message. 91 | * 92 | * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). 93 | * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 94 | * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) 95 | */ 96 | ArgumentException(String message, Throwable cause) { 97 | super(message, cause); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/FilterChain.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Calendar; 27 | import net.fortuna.ical4j.model.ComponentList; 28 | import net.fortuna.ical4j.model.component.CalendarComponent; 29 | import net.fortuna.ical4j.util.CompatibilityHints; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import java.util.List; 34 | import java.util.Optional; 35 | import java.util.concurrent.atomic.AtomicInteger; 36 | 37 | /** 38 | * Brings together multiple {@link ColanderFilter}s and applies them to all events of an iCal file. 39 | */ 40 | class FilterChain { 41 | static { 42 | CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_VALIDATION, true); 43 | } 44 | 45 | private static final Logger LOG = LoggerFactory.getLogger(FilterChain.class); 46 | 47 | private final List filters; 48 | 49 | public FilterChain(List filters) { 50 | this.filters = filters; 51 | } 52 | 53 | /** 54 | * Applies all filters of the chain to an iCal file. 55 | * 56 | * @param cal the iCal to parse 57 | * @return the modified iCal, never {@code null} 58 | */ 59 | Calendar run(Calendar cal) { 60 | LOG.info("Start processing. Please wait..."); 61 | AtomicInteger changedComponents = new AtomicInteger(0); 62 | // Create empty output calendar with same properties 63 | Calendar calOut = new Calendar(cal.getProperties(), new ComponentList<>()); 64 | 65 | ComponentList allComponents = cal.getComponents(); 66 | for (CalendarComponent component : allComponents) { 67 | int originalHashCode = component.hashCode(); 68 | filterEvent(component).ifPresent( filteredComponent -> { 69 | if (originalHashCode != filteredComponent.hashCode()) { 70 | changedComponents.incrementAndGet(); 71 | } 72 | calOut.getComponents().add(filteredComponent); 73 | }); 74 | } 75 | LOG.info("Number of components processed: {}", allComponents.size()); 76 | LOG.info("Number of components in new calendar: {}", calOut.getComponents().size()); 77 | LOG.info("Number of components deleted: {}", allComponents.size() - calOut.getComponents().size()); 78 | LOG.info("Number of components changed during filtering: {}", changedComponents.get()); 79 | 80 | return calOut; 81 | } 82 | 83 | /** 84 | * Visible for testing 85 | * @param component 86 | */ 87 | @SuppressWarnings("WeakerAccess") 88 | protected Optional filterEvent(CalendarComponent component) { 89 | CalendarComponent filteredComponent = component; 90 | for (ColanderFilter filter : filters) { 91 | Optional returnedEvent = filter.apply(filteredComponent); 92 | if (returnedEvent.isPresent()) { 93 | filteredComponent = returnedEvent.get(); 94 | } else { 95 | LOG.debug("Filter {} deleted originalEvent {}. Properties={}", filter, component.getName(), 96 | component.getProperties()); 97 | return Optional.empty(); 98 | } 99 | } 100 | return Optional.of(filteredComponent); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/RemoveDuplicateEventFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.component.CalendarComponent; 27 | import net.fortuna.ical4j.model.component.VEvent; 28 | import net.fortuna.ical4j.model.property.Description; 29 | import net.fortuna.ical4j.model.property.DtEnd; 30 | import net.fortuna.ical4j.model.property.DtStart; 31 | import net.fortuna.ical4j.model.property.Summary; 32 | 33 | import java.util.HashSet; 34 | import java.util.Optional; 35 | import java.util.Set; 36 | 37 | /** 38 | * Remove event when summary, description, start date or end date are the same in another event. 39 | */ 40 | public class RemoveDuplicateEventFilter extends TypedColanderFilter{ 41 | 42 | private Set filteredEvents = new HashSet<>(); 43 | 44 | @Override 45 | public Optional applyTyped(VEvent event) { 46 | ComparisonVEvent comparisonVEvent = new ComparisonVEvent(event); 47 | 48 | if (filteredEvents.contains(comparisonVEvent)) { 49 | return Optional.empty(); 50 | } else { 51 | filteredEvents.add(comparisonVEvent); 52 | return Optional.of(event); 53 | } 54 | } 55 | 56 | 57 | /** 58 | * Class that specifies the attributes of a {@link VEvent} that are compared when looking for "duplicates". 59 | */ 60 | private static class ComparisonVEvent extends VEvent { 61 | private Summary summary; 62 | private Description description; 63 | private DtStart startDate; 64 | private DtEnd endDate; 65 | 66 | ComparisonVEvent(VEvent eventToCompare) { 67 | this.summary = eventToCompare.getSummary(); 68 | this.description = eventToCompare.getDescription(); 69 | this.startDate = eventToCompare.getStartDate(); 70 | this.endDate = eventToCompare.getEndDate(); 71 | } 72 | 73 | @Override 74 | public boolean equals(Object o) { 75 | if (this == o) { 76 | return true; 77 | } 78 | if (o == null || getClass() != o.getClass()) { 79 | return false; 80 | } 81 | if (!super.equals(o)) { 82 | return false; 83 | } 84 | 85 | ComparisonVEvent that = (ComparisonVEvent) o; 86 | 87 | if (summary != null ? !summary.equals(that.summary) : that.summary != null) { 88 | return false; 89 | } 90 | if (description != null ? !description.equals(that.description) : that.description != null) { 91 | return false; 92 | } 93 | //noinspection SimplifiableIfStatement 94 | if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) { 95 | return false; 96 | } 97 | return endDate != null ? endDate.equals(that.endDate) : that.endDate == null; 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | int result = super.hashCode(); 103 | result = 31 * result + (summary != null ? summary.hashCode() : 0); 104 | result = 31 * result + (description != null ? description.hashCode() : 0); 105 | result = 31 * result + (startDate != null ? startDate.hashCode() : 0); 106 | result = 31 * result + (endDate != null ? endDate.hashCode() : 0); 107 | return result; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /cli/src/main/java/info/schnatterer/colander/cli/ColanderCli.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | import com.cloudogu.versionname.VersionName; 27 | import info.schnatterer.colander.Colander; 28 | import info.schnatterer.colander.cli.ArgumentsParser.ArgumentException; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | /** 33 | * Main class of Command Line Interface for colander. 34 | */ 35 | @VersionName 36 | class ColanderCli { 37 | private static final String PROGRAM_NAME = "colander"; 38 | private static final Logger LOG = LoggerFactory.getLogger(ColanderCli.class); 39 | 40 | /** 41 | * Main class should not be instantiated directly, only via {@link #main(String[])}. 42 | * Visible for testing. 43 | */ 44 | ColanderCli() { 45 | } 46 | 47 | /** 48 | * Entry point of the application 49 | * 50 | * @param args arguments passed via CLI 51 | */ 52 | public static void main(String[] args) { 53 | System.exit(new ColanderCli().execute(args).getExitStatus()); 54 | } 55 | 56 | /** 57 | * Parses {@code args} and starts {@link Colander}. 58 | * 59 | * @param args comand line args to parse. 60 | * @return an exit status to be returned to CLI. 61 | */ 62 | @SuppressWarnings({"squid:S1166", // Exceptions are logged in ArgumentsParser by contract. 63 | "squid:S2629" // Log statements are used for console output 64 | }) 65 | ExitStatus execute(String[] args) { 66 | LOG.info(createProgramNameWithVersion()); 67 | Arguments cliParams; 68 | try { 69 | cliParams = ArgumentsParser.read(args, PROGRAM_NAME); 70 | } catch (ArgumentException e) { 71 | return ExitStatus.ERROR_ARGS; 72 | } 73 | 74 | if (!cliParams.isHelp()) { 75 | return startColander(cliParams); 76 | } 77 | return ExitStatus.SUCCESS; 78 | } 79 | 80 | private String createProgramNameWithVersion() { 81 | return PROGRAM_NAME + " " + Version.NAME; 82 | } 83 | 84 | /** 85 | * Converts an {@link Arguments} object to a {@link Colander} and rinses. 86 | * 87 | * @param args comand line args to parse. 88 | * @return an exit status to be returned to CLI. 89 | */ 90 | ExitStatus startColander(Arguments args) { 91 | LOG.debug("CLI arguments={}", args); 92 | Colander.ColanderBuilder colander = createColanderBuilder(args.getInputFile()); 93 | if (args.isRemoveDuplicateEvents()) { 94 | colander.removeDuplicateEvents(); 95 | } 96 | if (args.isRemoveEmptyEvents()) { 97 | colander.removeEmptyEvents(); 98 | } 99 | args.getReplaceInSummary().forEach(colander::replaceInSummary); 100 | args.getReplaceInDescription().forEach(colander::replaceInDescription); 101 | args.getRemoveSummaryContains().forEach(colander::removeSummaryContains); 102 | args.getRemoveDescriptionContains().forEach(colander::removeDescriptionContains); 103 | 104 | try { 105 | colander.rinse().toFile(args.getOutputFile()); 106 | } catch (Exception e) { 107 | LOG.error("Error while parsing or writing calender: " + e.getMessage(), e); 108 | return ExitStatus.ERROR_PARSING; 109 | } 110 | return ExitStatus.SUCCESS; 111 | } 112 | 113 | /** 114 | * Visible for testing 115 | */ 116 | Colander.ColanderBuilder createColanderBuilder(String inputFile) { 117 | return Colander.toss(inputFile); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/ReplaceFilterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Date; 27 | import net.fortuna.ical4j.model.Property; 28 | import net.fortuna.ical4j.model.component.VEvent; 29 | import net.fortuna.ical4j.model.property.Description; 30 | import net.fortuna.ical4j.model.property.DtStart; 31 | import org.hamcrest.junit.ExpectedException; 32 | import org.junit.Rule; 33 | import org.junit.Test; 34 | 35 | import java.io.IOException; 36 | import java.net.URISyntaxException; 37 | import java.text.ParseException; 38 | 39 | import static org.assertj.core.api.Assertions.assertThat; 40 | import static org.mockito.Mockito.*; 41 | 42 | public class ReplaceFilterTest { 43 | private Date expectedDate = new Date(); 44 | 45 | @Rule 46 | public ExpectedException expectedException = ExpectedException.none(); 47 | 48 | @Test 49 | public void filterChangesOnMatch() throws Exception { 50 | ReplaceFilter filter = new ReplaceFilter("h.*llo", "hullo", Property.SUMMARY); 51 | VEvent event = new VEvent(expectedDate, "hallo icaltools"); 52 | VEvent expectedEvent = new VEvent(expectedDate, "hullo icaltools"); 53 | assertThat(filter.apply(event)).hasValue(expectedEvent); 54 | } 55 | 56 | @Test 57 | public void filterIgnoresWhenNoMatch() throws Exception { 58 | ReplaceFilter filter = new ReplaceFilter("hallo", "hullo", Property.SUMMARY); 59 | VEvent event = new VEvent(new Date(), "hullo icaltools"); 60 | assertThat(filter.apply(event)).hasValueSatisfying(actual -> assertThat(actual).isSameAs(event)); 61 | } 62 | 63 | @Test 64 | public void filterDescription() throws Exception { 65 | ReplaceFilter filter = new ReplaceFilter("h.*llo", "hullo", Property.DESCRIPTION); 66 | VEvent event = createVEvent(expectedDate, "hallo icaltools"); 67 | VEvent expectedEvent = createVEvent(expectedDate, "hullo icaltools"); 68 | assertThat(filter.apply(event)).hasValue(expectedEvent); 69 | } 70 | 71 | @Test 72 | public void filterPropertyDoesNotExist() throws Exception { 73 | ReplaceFilter filter = new ReplaceFilter("hallo", "hullo", Property.DESCRIPTION); 74 | VEvent event = new VEvent(expectedDate, "hullo icaltools"); 75 | // Unfiltered 76 | assertThat(filter.apply(event)).hasValue(event); 77 | } 78 | @Test 79 | public void filterPropertyDoesHaveValue() throws Exception { 80 | ReplaceFilter filter = new ReplaceFilter("hallo", "hullo", Property.SUMMARY); 81 | VEvent event = new VEvent(expectedDate, null); 82 | // Unfiltered 83 | assertThat(filter.apply(event)).hasValue(event); 84 | } 85 | 86 | @Test 87 | public void filterIOException() throws Exception { 88 | testException(new IOException("mocked Message")); 89 | } 90 | 91 | @Test 92 | public void filterURISyntaxException() throws Exception { 93 | testException(new URISyntaxException("uri", "mocked Message")); 94 | } 95 | 96 | @Test 97 | public void filterParseException() throws Exception { 98 | testException(new ParseException("mocked Message", 42)); 99 | } 100 | 101 | private VEvent createVEvent(Date startDate, String description) throws IOException, URISyntaxException, ParseException { 102 | VEvent event = new VEvent(); 103 | event.getProperties().add(new DtStart(startDate)); 104 | event.getProperties().add(new Description(description)); 105 | return event; 106 | } 107 | 108 | private void testException(Exception exception) throws IOException, URISyntaxException, ParseException { 109 | String expectedMessage = exception.getMessage(); 110 | ReplaceFilter filter = spy(new ReplaceFilter("", "", Property.SUMMARY)); 111 | doThrow(exception).when(filter).replace(any()); 112 | 113 | expectedException.expect(ColanderParserException.class); 114 | expectedException.expectMessage(expectedMessage); 115 | 116 | filter.apply(new VEvent(false)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/RemoveDuplicateEventFilterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Date; 27 | import net.fortuna.ical4j.model.component.VEvent; 28 | import net.fortuna.ical4j.model.property.Description; 29 | import net.fortuna.ical4j.model.property.DtEnd; 30 | import net.fortuna.ical4j.model.property.Summary; 31 | import org.junit.Before; 32 | import org.junit.Test; 33 | 34 | import java.time.LocalDateTime; 35 | import java.time.Month; 36 | import java.time.ZoneId; 37 | 38 | import static org.assertj.core.api.Assertions.assertThat; 39 | 40 | 41 | public class RemoveDuplicateEventFilterTest { 42 | private RemoveDuplicateEventFilter filter = new RemoveDuplicateEventFilter(); 43 | 44 | private LocalDateTime startDate = LocalDateTime.of(2012, Month.DECEMBER, 12, 13, 0); 45 | private LocalDateTime endDate = LocalDateTime.of(2012, Month.DECEMBER, 12, 23, 59); 46 | private LocalDateTime differentDate = LocalDateTime.of(2016, Month.JANUARY, 15, 12, 5); 47 | 48 | private VEvent event = createVEvent("Sum", "descr", startDate, endDate); 49 | 50 | @Before 51 | public void before() { 52 | // Initialize with one event 53 | assertThat(filter.apply(event)).hasValueSatisfying(actual -> assertThat(actual).isSameAs(event)); 54 | } 55 | 56 | @Test 57 | public void filterSameEvent() throws Exception { 58 | VEvent sameEvent = event; 59 | 60 | assertThat(filter.apply(event)).isEmpty(); 61 | assertThat(filter.apply(sameEvent)).isEmpty(); 62 | } 63 | 64 | @Test 65 | public void filterEqualEvent() throws Exception { 66 | VEvent equalEvent = 67 | new VEvent(event.getStartDate().getDate(), event.getEndDate().getDate(), event.getSummary().getValue()); 68 | equalEvent.getProperties().add(event.getDescription()); 69 | 70 | assertThat(filter.apply(equalEvent)).isEmpty(); 71 | } 72 | 73 | @Test 74 | public void filterDifferentSummary() throws Exception { 75 | VEvent differentSummary = createVEvent("DifferentSummary", "descr", startDate, endDate); 76 | 77 | assertThat(filter.apply(differentSummary)).hasValue(differentSummary); 78 | } 79 | 80 | @Test 81 | public void filterDifferentDescription() throws Exception { 82 | VEvent differentSummary = createVEvent("Sum", "DifferentDescr", startDate, endDate); 83 | 84 | assertThat(filter.apply(differentSummary)).hasValue(differentSummary); 85 | } 86 | 87 | @Test 88 | public void filterDifferentStartDate() throws Exception { 89 | VEvent differentStartDate = createVEvent("Sum", "descr", differentDate, endDate); 90 | 91 | assertThat(filter.apply(differentStartDate)).hasValue(differentStartDate); 92 | } 93 | 94 | @Test 95 | public void filterDifferentEndDate() throws Exception { 96 | VEvent differentEndDate = createVEvent("Sum", "descr", startDate, differentDate); 97 | 98 | assertThat(filter.apply(differentEndDate)).hasValue(differentEndDate); 99 | } 100 | 101 | @Test 102 | public void filterEndDateNull() throws Exception { 103 | VEvent endDateNull = new VEvent(toDate(startDate),"end date null"); 104 | 105 | assertThat(filter.apply(endDateNull)).hasValue(endDateNull); 106 | } 107 | 108 | @Test 109 | public void filterStartDateNull() throws Exception { 110 | VEvent startDateNull = new VEvent(); 111 | startDateNull.getProperties().add(new DtEnd(toDate(endDate))); 112 | startDateNull.getProperties().add(new Summary("start date null")); 113 | 114 | assertThat(filter.apply(startDateNull)).hasValue(startDateNull); 115 | } 116 | 117 | private VEvent createVEvent(String sum, String descr, LocalDateTime startDate, LocalDateTime endDate) { 118 | VEvent event = new VEvent(toDate(startDate), toDate(endDate), sum); 119 | event.getProperties().add(new Description(descr)); 120 | return event; 121 | } 122 | 123 | private Date toDate(LocalDateTime of) { 124 | return new Date(java.util.Date.from(of.atZone(ZoneId.systemDefault()).toInstant())); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cli/src/test/java/info/schnatterer/colander/cli/ColanderCliTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | import info.schnatterer.colander.Colander; 27 | import org.junit.Before; 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | import org.mockito.Mock; 31 | import org.mockito.Spy; 32 | import org.mockito.junit.MockitoJUnitRunner; 33 | 34 | import java.util.Arrays; 35 | import java.util.HashMap; 36 | 37 | import static org.junit.Assert.assertEquals; 38 | import static org.mockito.Mockito.*; 39 | 40 | @RunWith(MockitoJUnitRunner.class) 41 | public class ColanderCliTest { 42 | 43 | @Spy 44 | private ColanderCli cli; 45 | 46 | @Mock 47 | private Colander.ColanderBuilder builder; 48 | 49 | @Mock 50 | private Colander.ColanderResult result; 51 | 52 | @Mock 53 | private Arguments args; 54 | 55 | @Before 56 | public void before() throws Exception { 57 | doReturn(builder).when(cli).createColanderBuilder(any()); 58 | when(builder.rinse()).thenReturn(result); 59 | } 60 | 61 | @Test 62 | public void execute() throws Exception { 63 | assertEquals("Exit status", ExitStatus.SUCCESS, execute("a/b.ical")); 64 | } 65 | 66 | @Test 67 | public void executeHelp() throws Exception { 68 | assertEquals("Exit status", ExitStatus.SUCCESS, execute("--help")); 69 | verifyZeroInteractions(builder, result); 70 | } 71 | 72 | @Test 73 | public void executeErrorArgs() throws Exception { 74 | assertEquals("Exit status", ExitStatus.ERROR_ARGS, execute("--wtf")); 75 | } 76 | 77 | @Test 78 | public void executeErrorParsing() throws Exception { 79 | when(builder.rinse()).thenThrow(new RuntimeException("Mocked exception")); 80 | assertEquals("Exit status", ExitStatus.ERROR_PARSING, execute("a/b.ical")); 81 | } 82 | 83 | @Test 84 | public void startColander() throws Exception { 85 | String expectedInput = "in"; 86 | String expectedOutput = "out"; 87 | when(args.getInputFile()).thenReturn(expectedInput); 88 | when(args.getOutputFile()).thenReturn(expectedOutput); 89 | when(args.isRemoveDuplicateEvents()).thenReturn(true); 90 | when(args.isRemoveEmptyEvents()).thenReturn(true); 91 | when(args.getRemoveSummaryContains()).thenReturn(Arrays.asList("a", "b")); 92 | when(args.getRemoveDescriptionContains()).thenReturn(Arrays.asList("y", "z")); 93 | when(args.getReplaceInSummary()).thenReturn(new HashMap() {{ 94 | put("a", "b"); 95 | put("c", "d"); 96 | }}); 97 | when(args.getReplaceInDescription()).thenReturn(new HashMap() {{ 98 | put("1", "2"); 99 | put("3", "4"); 100 | }}); 101 | 102 | assertEquals("Exit status", ExitStatus.SUCCESS, cli.startColander(args)); 103 | 104 | verify(cli).createColanderBuilder(expectedInput); 105 | verify(builder).removeDuplicateEvents(); 106 | verify(builder).removeEmptyEvents(); 107 | verify(builder).replaceInSummary("a", "b"); 108 | verify(builder).replaceInSummary("c", "d"); 109 | verify(builder).replaceInDescription("1", "2"); 110 | verify(builder).replaceInDescription("3", "4"); 111 | verify(builder).removeSummaryContains("a"); 112 | verify(builder).removeSummaryContains("b"); 113 | verify(builder).removeDescriptionContains("y"); 114 | verify(builder).removeDescriptionContains("z"); 115 | verify(result).toFile(expectedOutput); 116 | } 117 | 118 | @Test 119 | public void startColanderEmptyArgs() throws Exception { 120 | assertEquals("Exit status", ExitStatus.SUCCESS, cli.startColander(args)); 121 | 122 | verify(cli).createColanderBuilder(null); 123 | verify(builder, never()).removeDuplicateEvents(); 124 | verify(builder, never()).removeEmptyEvents(); 125 | verify(builder, never()).replaceInSummary(anyString(), anyString()); 126 | verify(builder, never()).removeSummaryContains(anyString()); 127 | verify(builder, never()).removeDescriptionContains(anyString()); 128 | verify(result).toFile(null); 129 | } 130 | 131 | private ExitStatus execute(String... args) { 132 | return cli.execute(args); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📆 colander - filtering your calendar 2 | 3 | [![Docker Image](https://images.microbadger.com/badges/image/schnatterer/colander.svg)](https://hub.docker.com/r/schnatterer/colander/) 4 | [![Build Status](https://travis-ci.org/schnatterer/colander.svg?branch=develop)](https://travis-ci.org/schnatterer/colander) 5 | [![Quality Gates](https://sonarcloud.io/api/project_badges/measure?project=info.schnatterer.colander%3Acolander-parent&metric=alert_status)](https://sonarcloud.io/dashboard?id=info.schnatterer.colander%3Acolander-parent) 6 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=info.schnatterer.colander%3Acolander-parent&metric=coverage)](https://sonarcloud.io/dashboard?id=info.schnatterer.colander%3Acolander-parent) 7 | [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=info.schnatterer.colander%3Acolander-parent&metric=sqale_index)](https://sonarcloud.io/dashboard?id=info.schnatterer.colander%3Acolander-parent) 8 | [![JitPack](https://www.jitpack.io/v/schnatterer/colander.svg)](https://www.jitpack.io/#schnatterer/colander) 9 | [![License](https://img.shields.io/github/license/schnatterer/colander.svg)](LICENSE) 10 | 11 | Colander filters calender in ICS files. It can either be used as standalone application via [command line interface](#cli) or within 12 | JVM applications using the [API](#api). 13 | 14 | # CLI 15 | 16 | * Download the latest version from [Releases](https://github.com/schnatterer/colander/releases). 17 | * Extract the zip file. 18 | * Use ist as follows: 19 | ``` 20 | Usage: colander [options] [ 21 | Options: 22 | --help 23 | (optional) Show this message 24 | Default: false 25 | --remove-description 26 | Remove calender component when description contains expression 27 | Default: [] 28 | --remove-duplicate-events 29 | Remove event when summary, description, start date or end date are the 30 | same in another event 31 | Default: false 32 | --remove-empty-events 33 | Remove events when summary and description are empty 34 | Default: false 35 | --remove-summary 36 | Remove calender component when summary contains expression 37 | Default: [] 38 | --replace-description 39 | Replace in description of calender components (regex) 40 | Syntax: --replace-descriptionkey=value 41 | Default: {} 42 | --replace-summary 43 | Replace in summary calender components (regex) 44 | Syntax: --replace-summarykey=value 45 | Default: {} 46 | ``` 47 | * Example 48 | ``` 49 | colander --remove-summary "Remove, RemoveIncludingLeadingSpace" --remove-summary "Another One to remove" --replace-summary "l.ne=line" cal.ics cal-new.ics 50 | ``` 51 | * Note that 52 | * filters might refer to specific calender components (such as events). If not otherwise noted, a filter applies to all calender components (tasks, ToDos, Alarms, Venues, etc.) 53 | * the order of the arguments/filters is not maintained. That is, they are not applied in the order as passed 54 | to the CLI. 55 | * If no `output.ics` file is passed, colander creates one, basing on the file name and the current timestamp, e.g. `input-20170129194742.ics`. 56 | * Colander never overwrites existing files. If the `output.ics` exists, colander fails. 57 | * If you care about return codes, they can be found here: [ExitStatus](cli/src/main/java/info/schnatterer/colander/cli/ExitStatus.java)) 58 | * Another example is the integration test for CLI (see [ColanderCliITCase](cli/src/test/java/info/schnatterer/colander/cli/ColanderCliITCase.java)). 59 | * Colander CLI writes logs to the `logs` folder. 60 | 61 | # API 62 | 63 | The basic logic of colander is wrapped in the core module. This can be reused in other applications. 64 | For now, this is not hosted on maven central, but on your can get it via jitpack. 65 | 66 | Add the following maven repository to your POM.xml 67 | 68 | ```xml 69 | 70 | 71 | jitpack.io 72 | https://jitpack.io 73 | 74 | 75 | ``` 76 | 77 | Then add the actual dependency 78 | 79 | ```xml 80 | 81 | com.github.schnatterer.colander 82 | colander-core 83 | 0.1.0 84 | 85 | ``` 86 | 87 | ## How to use 88 | 89 | ```java 90 | Colander.toss("/some/input.ics") 91 | .removeDuplicateEvents() 92 | .removeEmptyEvents() 93 | .removePropertyContains(Property.SUMMARY, "Remove me") 94 | .removeDescriptionContains("Remove me 2") 95 | // Generic replace in property 96 | .replaceInProperty(Property.DESCRIPTION, "L.ne", "Line") 97 | // Convenience: replace in property summary 98 | .replaceInSummary("Replace", "Replace!") 99 | .filter(event -> { 100 | System.out.println(event.toString()); 101 | return Optional.of(event); 102 | }) 103 | .rinse() 104 | .toFile("/some/output.ics"); 105 | ``` 106 | 107 | Under the hood, colander uses [ical4j](https://github.com/ical4j/ical4j). You can get an instance of the result like so 108 | 109 | ```java 110 | Calendar cal = Colander.toss("/some/input.ics") 111 | // ... 112 | .rinse() 113 | .toCalendar("/some/output.ics"); 114 | ``` 115 | 116 | More examples can be found in the 117 | * CLI module (see [ColanderCli](cli/src/main/java/info/schnatterer/colander/cli/ColanderCli.java)) and 118 | * integration test for core (see [ColanderITCase](core/src/test/java/info/schnatterer/colander/ColanderITCase.java)) 119 | 120 | -------------------------------------------------------------------------------- /cli/src/main/java/info/schnatterer/colander/cli/Arguments.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | import com.beust.jcommander.DynamicParameter; 27 | import com.beust.jcommander.Parameter; 28 | 29 | import java.util.ArrayList; 30 | import java.util.HashMap; 31 | import java.util.List; 32 | import java.util.Map; 33 | 34 | /** 35 | * Value object that holds the arguments passed to CLI. 36 | */ 37 | // JCommander involves a lot of annotation magic, which leads to a lot of warnings which don't really apply here 38 | @SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection", "FieldCanBeLocal"}) 39 | public class Arguments { 40 | 41 | /** List of unnamed arguments.*/ 42 | @Parameter(required = true, description = " [") 43 | private List mainArguments = new ArrayList<>(); 44 | 45 | @DynamicParameter(names = "--replace-summary", description = "Replace in summary calender components (regex)") 46 | private Map replaceInSummary = new HashMap<>(); 47 | 48 | @DynamicParameter(names = "--replace-description", description = "Replace in description of calender components (regex)") 49 | private Map replaceInDescription = new HashMap<>(); 50 | 51 | @Parameter(names = "--remove-summary", description = "Remove calender component when summary contains expression") 52 | private List removeSummaryContains = new ArrayList<>(); 53 | 54 | @Parameter(names = "--remove-description", description = "Remove calender component when description contains expression") 55 | private List removeDescriptionContains = new ArrayList<>(); 56 | 57 | @Parameter(names = "--remove-duplicate-events", description = "Remove event when summary, description, start date or end date are the same in another event") 58 | private boolean removeDuplicateEvents = false; 59 | 60 | @Parameter(names = "--remove-empty-events", description = "Remove events when summary and description are empty") 61 | private boolean removeEmptyEvents = false; 62 | 63 | @Parameter(names = "--help", help = true, description = "(optional) Show this message") 64 | private boolean help; 65 | 66 | /** 67 | * @return input file name. Never {@code null}. 68 | */ 69 | public String getInputFile() { 70 | return mainArguments.get(0); 71 | } 72 | 73 | /** 74 | * @return output file name. Can be {@code null}! 75 | */ 76 | public String getOutputFile() { 77 | String outputFile = null; 78 | if (mainArguments.size() > 1) { 79 | outputFile = mainArguments.get(1); 80 | } 81 | return outputFile; 82 | } 83 | 84 | /** 85 | * @return pairs of regexes to be replaced by each other within the summary. Replace key by value. Never {@code null}. 86 | */ 87 | public Map getReplaceInSummary() { return replaceInSummary; } 88 | 89 | /** 90 | * @return pairs of regexes to be replaced by each other within the description. Replace key by value. Never {@code null}. 91 | */ 92 | public Map getReplaceInDescription() { return replaceInDescription; } 93 | 94 | /** 95 | * @return the terms that when contained in summary, lead to removal. 96 | */ 97 | public List getRemoveSummaryContains() { return removeSummaryContains; } 98 | 99 | /** 100 | * @return the terms that when contained in description, lead to removal. 101 | */ 102 | public List getRemoveDescriptionContains() { 103 | return removeDescriptionContains; 104 | } 105 | 106 | /** 107 | * @return {@code true} when duplicates should be removed. Otherwise {@code false}. 108 | */ 109 | public boolean isRemoveDuplicateEvents() { return removeDuplicateEvents; } 110 | 111 | /** 112 | * @return {@code true} when empty events should be removed. Otherwise {@code false}. 113 | */ 114 | public boolean isRemoveEmptyEvents() { return removeEmptyEvents; } 115 | 116 | /** 117 | * @return {@code true} when help argument was passed. Otherwise {@code false}. 118 | */ 119 | public boolean isHelp() { return help; } 120 | 121 | @Override 122 | public String toString() { 123 | return "Arguments{" + 124 | "mainArguments=" + mainArguments + 125 | ", replaceInSummary=" + replaceInSummary + 126 | ", replaceInDescription=" + replaceInDescription + 127 | ", removeSummaryContains=" + removeSummaryContains + 128 | ", removeDuplicateEvents=" + removeDuplicateEvents + 129 | ", removeEmptyEvents=" + removeEmptyEvents + 130 | ", help=" + help + 131 | '}'; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /test-lib/src/main/java/info/schnatterer/colander/test/ITCases.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.test; 25 | 26 | import info.schnatterer.colander.Properties; 27 | import net.fortuna.ical4j.data.CalendarBuilder; 28 | import net.fortuna.ical4j.data.ParserException; 29 | import net.fortuna.ical4j.model.Calendar; 30 | import net.fortuna.ical4j.model.ComponentList; 31 | import net.fortuna.ical4j.model.Property; 32 | import net.fortuna.ical4j.model.component.CalendarComponent; 33 | import org.junit.rules.TemporaryFolder; 34 | 35 | import java.io.*; 36 | import java.nio.file.Files; 37 | import java.util.Arrays; 38 | import java.util.HashSet; 39 | import java.util.List; 40 | import java.util.Set; 41 | import java.util.stream.Collectors; 42 | 43 | import static org.junit.Assert.assertEquals; 44 | import static org.junit.Assert.assertTrue; 45 | 46 | /** 47 | * Constants used for testing in all modules. 48 | */ 49 | public class ITCases { 50 | private static final String ICS_FILE = "ColanderIT.ics"; 51 | 52 | private ITCases() { 53 | } 54 | 55 | /** 56 | * @return the absolute file path to test ICS file 57 | */ 58 | public static String getFilePathTestIcs(TemporaryFolder folder) throws IOException { 59 | return getFilePathTestIcs(ICS_FILE, folder); 60 | } 61 | 62 | /** 63 | * Verifies that a parsed ICS is as expected. 64 | */ 65 | @SuppressWarnings("squid:S1160") // This is a test-lib. Don't try to win a trophy for its design. 66 | public static void verifyParsedIcs(String inputPath, String outputPath) throws IOException, ParserException { 67 | Calendar originalCal = new CalendarBuilder().build(new FileInputStream(inputPath)); 68 | Calendar filteredCal = new CalendarBuilder().build(new FileInputStream(outputPath)); 69 | List filteredComponents = filteredCal.getComponents(); 70 | ComponentList originalComponents = originalCal.getComponents(); 71 | assertEquals("Number of components", originalComponents.size() - 6L, filteredComponents.size()); 72 | CalendarComponent duplicate = findComponentBySummary(filteredComponents, "Duplicate"); 73 | CalendarComponent replacedEvent = findComponentBySummary(filteredComponents, "event Replace"); 74 | assertEquals("Replaced event description", "FirstLine\nSecondLine\nThirdLine\n", 75 | replacedEvent.getProperty(Property.DESCRIPTION).getValue()); 76 | assertEquals("Replaced event summary", "event Replace!", 77 | replacedEvent.getProperty(Property.SUMMARY).getValue()); 78 | 79 | CalendarComponent replacedToDo = findComponentBySummary(filteredComponents, "TDO Replace"); 80 | assertEquals("Replaced todo summary", "TDO Replace!", 81 | replacedToDo.getProperty(Property.SUMMARY).getValue()); 82 | 83 | // Check unfiltered components 84 | Set changedComponents = new HashSet<>(Arrays.asList(duplicate, replacedEvent, replacedToDo)); 85 | filteredComponents.stream() 86 | .filter(component -> !changedComponents.contains(component)) 87 | .forEach(unchangedComponent -> 88 | assertTrue("Unfiltered calender component not found in inut calender. Was it changed? Component: " 89 | + unchangedComponent, originalComponents.contains(unchangedComponent))); 90 | } 91 | 92 | /** 93 | * Visible for testing 94 | */ 95 | static String getFilePathTestIcs(String path, TemporaryFolder folder) throws IOException { 96 | InputStream testIcsFileStream = ITCases.class.getClassLoader().getResourceAsStream(path); 97 | if (testIcsFileStream == null) { 98 | throw new AssertionError("Test ICS file not found"); 99 | } 100 | 101 | // Write ics file to temporary folder, to also work when this module is a jar dependecy 102 | File newIcsFile = folder.newFile(); 103 | Files.write(newIcsFile.toPath(), read(testIcsFileStream).getBytes("UTF-8")); 104 | return newIcsFile.getAbsolutePath(); 105 | } 106 | 107 | private static CalendarComponent findComponentBySummary(List events, String summaryContains) { 108 | List filteredEvents = events.stream() 109 | .filter(event -> Properties.getSummaryValue(event).map(value -> value.contains(summaryContains)).orElse(false)) 110 | .collect(Collectors.toList()); 111 | assertEquals("Expected number of events with summary: " + summaryContains, 1, filteredEvents.size()); 112 | return filteredEvents.get(0); 113 | } 114 | 115 | private static String read(InputStream input) throws IOException { 116 | try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { 117 | return buffer.lines().collect(Collectors.joining("\n")); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | /** 3 | * The MIT License (MIT) 4 | * 5 | * Copyright (c) 2017 Johannes Schnatterer 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | 26 | node { // No specific label 27 | 28 | properties([ 29 | // Keep only the last 10 build to preserve space 30 | //buildDiscarder(logRotator(numToKeepStr: '10')), 31 | [$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '10']], 32 | // Configure GitHub project in order to start builds on push 33 | [$class: 'GithubProjectProperty', projectUrlStr: 'https://github.com/schnatterer/colander'], 34 | pipelineTriggers([[$class: 'GitHubPushTrigger']]), 35 | // Don't run concurrent builds for a branch, because they use the same workspace directory 36 | disableConcurrentBuilds() 37 | ]) 38 | 39 | def CREDENTIALS = [ 40 | $class : 'StringBinding', 41 | credentialsId: 'sonarqube', 42 | variable : 'authToken' 43 | ] 44 | 45 | //def sonarQubeVersion = 'sonar5-6' 46 | // TODO once withSonarQubeEnv closure works, use sonarQubeVersion and remove other SQ properties bellow 47 | String SONAR_MAVEN_PLUGIN_VERSION = '3.2' 48 | String SONAR_HOST_URL = env.SONAR_HOST 49 | 50 | String emailRecipients = env.EMAIL_RECIPIENTS 51 | 52 | catchError { 53 | 54 | def mvnHome = tool 'M3.3' 55 | def javaHome = tool 'JDK8' 56 | 57 | Maven mvn = new Maven(this, mvnHome, javaHome) 58 | if ("master".equals(env.BRANCH_NAME)) { 59 | mvn.additionalArgs = "-DperformRelease" 60 | currentBuild.description = mvn.getVersion() 61 | } 62 | 63 | stage('Checkout') { 64 | checkout scm 65 | gitClean() 66 | } 67 | 68 | stage('Build') { 69 | // Run the maven build 70 | mvn 'clean install -DskipTests' 71 | archive '**/target/*.jar,**/target/*.zip' 72 | } 73 | 74 | //parallel unitTests: { 75 | stage('Unit Test') { 76 | mvn 'test' 77 | } 78 | //}, integrationTests: { 79 | stage('Integration Test') { 80 | mvn 'verify -DskipUnitTests' 81 | } 82 | //}, failFast: true 83 | 84 | stage('SonarQube') { 85 | //withSonarQubeEnv(sonarQubeVersion) { 86 | // Results in this error https://issues.jenkins-ci.org/browse/JENKINS-39346 87 | // mvn "$SONAR_MAVEN_GOAL -Dsonar.host.url=$SONAR_HOST_URL", 88 | // // exclude generated code in target folder 89 | // "-Dsonar.exclusions=target/**" 90 | //} 91 | 92 | withCredentials([CREDENTIALS]) { 93 | //noinspection GroovyAssignabilityCheck 94 | mvn "org.codehaus.mojo:sonar-maven-plugin:${SONAR_MAVEN_PLUGIN_VERSION}:sonar -Dsonar.host.url=${SONAR_HOST_URL} " + 95 | "-Dsonar.login=$authToken " + 96 | // Exclude generated code in target folder 97 | "-Dsonar.exclusions=target/** " 98 | //+ sonarBranchProperty 99 | } 100 | } 101 | } 102 | 103 | // Archive Unit and integration test results, if any 104 | junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml' 105 | 106 | // email on fail 107 | step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: emailRecipients, sendToIndividuals: true]) 108 | } 109 | 110 | class Maven implements Serializable { 111 | def mvnHome 112 | def javaHome 113 | def script 114 | 115 | // Args added to each mvn call 116 | String additionalArgs = "" 117 | 118 | Maven(script, mvnHome, javaHome) { 119 | this.script = script 120 | this.mvnHome = mvnHome 121 | this.javaHome = javaHome 122 | } 123 | 124 | def call(args) { 125 | mvn(args) 126 | } 127 | 128 | def mvn(String args) { 129 | // Apache Maven related side notes: 130 | // --batch-mode : recommended in CI to inform maven to not run in interactive mode (less logs) 131 | // -V : strongly recommended in CI, will display the JDK and Maven versions in use. 132 | // Very useful to be quickly sure the selected versions were the ones you think. 133 | // -U : force maven to update snapshots each time (default : once an hour, makes no sense in CI). 134 | // -Dsurefire.useFile=false : useful in CI. Displays test errors in the logs directly (instead of 135 | // having to crawl the workspace files to see the cause). 136 | 137 | // Advice: don't define M2_HOME in general. Maven will autodetect its root fine. 138 | script.withEnv(["JAVA_HOME=${javaHome}", "PATH+MAVEN=${mvnHome}/bin:${script.env.JAVA_HOME}/bin"]) { 139 | script.sh "${mvnHome}/bin/mvn --batch-mode -V -U -e -Dsurefire.useFile=false --settings ${script.env.HOME}/.m2/settings.xml ${args + " " + additionalArgs}" 140 | } 141 | } 142 | 143 | String getVersion() { 144 | def matcher = script.readFile('pom.xml') =~ '(.+)' 145 | matcher ? matcher[0][1] : null 146 | } 147 | } 148 | 149 | void gitClean() { 150 | // Remove all untracked files 151 | sh "git clean -df" 152 | //Clear all unstaged changes 153 | sh 'git checkout -- .' 154 | } 155 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/FilterChainTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Calendar; 27 | import net.fortuna.ical4j.model.ComponentList; 28 | import net.fortuna.ical4j.model.Date; 29 | import net.fortuna.ical4j.model.component.*; 30 | import net.fortuna.ical4j.model.property.Summary; 31 | import org.junit.Before; 32 | import org.junit.Test; 33 | import org.mockito.invocation.InvocationOnMock; 34 | import org.mockito.stubbing.Answer; 35 | 36 | import java.util.Arrays; 37 | import java.util.List; 38 | import java.util.Optional; 39 | 40 | import static org.assertj.core.api.Assertions.assertThat; 41 | import static org.junit.Assert.assertFalse; 42 | import static org.junit.Assert.assertTrue; 43 | import static org.mockito.ArgumentMatchers.any; 44 | import static org.mockito.Mockito.*; 45 | 46 | public class FilterChainTest { 47 | 48 | private ColanderFilter passThroughFilter1 = mock(ColanderFilter.class); 49 | private ColanderFilter passThroughFilter2 = mock(ColanderFilter.class); 50 | private VEvent inputEvent = new VEvent(); 51 | 52 | @Before 53 | public void setUp() { 54 | when(passThroughFilter1.apply(any(VEvent.class))).thenAnswer(new PassThroughAnswer()); 55 | when(passThroughFilter2.apply(any(VEvent.class))).thenAnswer(new PassThroughAnswer()); 56 | inputEvent.getProperties().add(new Summary("")); 57 | } 58 | 59 | @Test 60 | public void testParse() { 61 | FilterChain pipe = new FilterChain(Arrays.asList(passThroughFilter1, passThroughFilter2)); 62 | 63 | VEvent event1 = new VEvent(new Date(), "event1"); 64 | VEvent event2 = new VEvent(new Date(), "event2"); 65 | List otherComponents = Arrays.asList(new VToDo(), new VTimeZone(), new VAlarm(), new VFreeBusy(), 66 | new VAvailability(), new VVenue(), new VJournal(), new XComponent("xcomp")); 67 | Calendar inputCalender = new Calendar(new ComponentList() {{ 68 | add(event1); 69 | add(event2); 70 | addAll(otherComponents); 71 | }}); 72 | 73 | Calendar outputCalendar = pipe.run(inputCalender); 74 | verify(passThroughFilter1).apply(event1); 75 | verify(passThroughFilter1).apply(event2); 76 | verify(passThroughFilter2).apply(event1); 77 | verify(passThroughFilter2).apply(event2); 78 | assertTrue("Event 1 not in output calender", outputCalendar.getComponents().contains(event1)); 79 | assertTrue("Event 2 not in output calender", outputCalendar.getComponents().contains(event2)); 80 | otherComponents.forEach( calendarComponent -> outputCalendar.getComponents().contains(calendarComponent)); 81 | } 82 | 83 | @Test 84 | public void testParseDelete() { 85 | 86 | VEvent event1 = new VEvent(new Date(), "event1"); 87 | VEvent event2 = new VEvent(new Date(), "event2"); 88 | Calendar inputCalender = new Calendar(new ComponentList() {{ 89 | add(event1); 90 | add(event2); 91 | }}); 92 | 93 | ColanderFilter deleteEventFilter = mock(ColanderFilter.class); 94 | when(deleteEventFilter.apply(any(VEvent.class))).thenAnswer(new PassThroughAnswer()); 95 | when(deleteEventFilter.apply(event1)).thenReturn(Optional.empty()); 96 | FilterChain pipe = new FilterChain(Arrays.asList(passThroughFilter1, deleteEventFilter, passThroughFilter2)); 97 | 98 | Calendar outputCalendar = pipe.run(inputCalender); 99 | verify(passThroughFilter1).apply(event1); 100 | verify(passThroughFilter1).apply(event2); 101 | verify(deleteEventFilter).apply(event1); 102 | verify(deleteEventFilter).apply(event2); 103 | verify(passThroughFilter2, never()).apply(event1); 104 | verify(passThroughFilter2).apply(event2); 105 | assertFalse("Event 1 in output calender", outputCalendar.getComponents().contains(event1)); 106 | assertTrue("Event 2 not in output calender", outputCalendar.getComponents().contains(event2)); 107 | } 108 | 109 | @Test 110 | public void testFilterEvent() { 111 | FilterChain pipe = new FilterChain(Arrays.asList(passThroughFilter1, passThroughFilter2)); 112 | 113 | Optional actualFilteredEvent = pipe.filterEvent(inputEvent); 114 | verify(passThroughFilter1).apply(inputEvent); 115 | verify(passThroughFilter2).apply(inputEvent); 116 | assertThat(actualFilteredEvent).hasValue(inputEvent); 117 | } 118 | 119 | @Test 120 | public void testFilterEventDelete() { 121 | ColanderFilter filter2 = mock(ColanderFilter.class); 122 | FilterChain pipe = new FilterChain(Arrays.asList(passThroughFilter1, filter2, passThroughFilter2)); 123 | 124 | Optional vEvent = pipe.filterEvent(inputEvent); 125 | verify(passThroughFilter1).apply(inputEvent); 126 | verify(filter2).apply(inputEvent); 127 | verify(passThroughFilter2, never()).apply(inputEvent); 128 | assertThat(vEvent).withFailMessage("Event not deleted").isEmpty(); 129 | } 130 | 131 | private static class PassThroughAnswer implements Answer> { 132 | @Override 133 | public Optional answer(InvocationOnMock invocationOnMock) throws Throwable { 134 | return Optional.of((VEvent) invocationOnMock.getArguments()[0]); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/ColanderIO.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.data.CalendarBuilder; 27 | import net.fortuna.ical4j.data.CalendarOutputter; 28 | import net.fortuna.ical4j.data.ParserException; 29 | import net.fortuna.ical4j.model.Calendar; 30 | import net.fortuna.ical4j.validate.ValidationException; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | import java.io.*; 35 | import java.nio.file.FileAlreadyExistsException; 36 | import java.time.LocalDateTime; 37 | import java.time.format.DateTimeFormatter; 38 | 39 | /** 40 | * Handles in and output of calenders conveniently. 41 | */ 42 | class ColanderIO { 43 | private static final Logger LOG = LoggerFactory.getLogger(ColanderIO.class); 44 | static final String DATE_TIME_FORMAT_FILE_NAME = "yyyyMMddHHmmss"; 45 | 46 | /** 47 | * Creates calendar object from an ical file from a create a calender object 48 | * 49 | * @param filePath the path to the ical file 50 | * @return an object representing the ical file 51 | * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for 52 | * some other reason cannot be opened forreading. 53 | * @throws IOException where an error occurs reading data from the specified stream 54 | * @throws ColanderParserException where an error occurs parsing data from the stream 55 | */ 56 | Calendar read(String filePath) throws IOException { 57 | return read(new FileInputStream(filePath)); 58 | } 59 | 60 | /** 61 | * Creates calendar object from an ical stream from a create a calender object 62 | * 63 | * @param input a stream containg the ical file 64 | * @return an object representing the ical file 65 | * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for 66 | * some other reason cannot be opened forreading. 67 | * @throws IOException where an error occurs reading data from the specified stream 68 | * @throws ColanderParserException where an error occurs parsing data from the stream 69 | */ 70 | Calendar read(InputStream input) throws IOException { 71 | LOG.info("Reading calendar file..."); 72 | 73 | try { 74 | return createCalenderBuilder().build(input); 75 | } catch (ParserException e) { 76 | throw new ColanderParserException(e); 77 | } 78 | } 79 | 80 | /** 81 | * Writes a calender object to a file. 82 | * 83 | * @param cal the iCal to write 84 | * @param outputPath the file to write the modified iCal file to. When {@code null}, a new filename is generated 85 | * from {@code inputFilePath}. 86 | * @param inputFilePath input file path. Only needed when output path is {@code null}. 87 | * @throws FileNotFoundException if the file exists but is a directory 88 | * rather than a regular file, does not exist but cannot 89 | * be created, or cannot be opened for any other reason 90 | * @throws IOException thrown when unable to write to output stream 91 | * @throws ColanderParserException where calendar validation fails 92 | * @throws FileAlreadyExistsException if the file exists. Colander is not going to overwrite any files. 93 | */ 94 | void write(Calendar cal, String outputPath, String inputFilePath) throws IOException { 95 | String actualPath = outputPath; 96 | if (actualPath == null) { 97 | actualPath = generateOutputPath(inputFilePath); 98 | } 99 | if (new File(actualPath).exists()) { 100 | throw new FileAlreadyExistsException(actualPath, null, "File already exists. Not going to overwrite it."); 101 | } 102 | LOG.info("Writing output to {}", actualPath); 103 | try (OutputStream outputStream = createOutputStream(actualPath)) { 104 | CalendarOutputter calendarOutputter = createCalendarOutputter(); 105 | try { 106 | calendarOutputter.output(cal, outputStream); 107 | } catch (ValidationException e) { 108 | throw new ColanderParserException(e); 109 | } 110 | } 111 | } 112 | 113 | private String generateOutputPath(String inputFilePath) { 114 | if (inputFilePath == null) { 115 | throw new ColanderParserException("Both input and output file paths are null. Can't write result."); 116 | } 117 | 118 | int extensionSeparator = inputFilePath.lastIndexOf('.'); 119 | if (extensionSeparator < 0) { 120 | extensionSeparator = inputFilePath.length(); 121 | } 122 | return inputFilePath.substring(0, extensionSeparator) 123 | + '-' 124 | + LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_FILE_NAME)) 125 | + inputFilePath.substring(extensionSeparator); 126 | } 127 | 128 | /** 129 | * Visible for testing 130 | */ 131 | CalendarBuilder createCalenderBuilder() { return new CalendarBuilder(); } 132 | 133 | /** 134 | * Visible for testing 135 | */ 136 | CalendarOutputter createCalendarOutputter() { 137 | // Don't validate output. SISO. 138 | return new CalendarOutputter(false); 139 | } 140 | 141 | /** 142 | * Visible for testing 143 | */ 144 | OutputStream createOutputStream(String outputFile) throws FileNotFoundException { 145 | return new FileOutputStream(outputFile); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /cli/src/test/java/info/schnatterer/colander/cli/ArgumentsParserTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander.cli; 25 | 26 | import info.schnatterer.colander.cli.ArgumentsParser.ArgumentException; 27 | import org.hamcrest.junit.ExpectedException; 28 | import org.junit.Rule; 29 | import org.junit.Test; 30 | import uk.org.lidalia.slf4jtest.LoggingEvent; 31 | import uk.org.lidalia.slf4jtest.TestLogger; 32 | import uk.org.lidalia.slf4jtest.TestLoggerFactory; 33 | import uk.org.lidalia.slf4jtest.TestLoggerFactoryResetRule; 34 | 35 | import java.util.List; 36 | import java.util.Map; 37 | 38 | import static org.hamcrest.Matchers.*; 39 | import static org.junit.Assert.*; 40 | 41 | public class ArgumentsParserTest { 42 | private static final String PROGRAM_NAME = "progr"; 43 | 44 | @Rule 45 | public ExpectedException expectedException = ExpectedException.none(); 46 | 47 | /** Logger of class under test. */ 48 | private static final TestLogger LOG = TestLoggerFactory.getTestLogger(ArgumentsParser.class); 49 | 50 | /** Rest logger before each test. **/ 51 | @Rule 52 | public TestLoggerFactoryResetRule testLoggerFactoryResetRule = new TestLoggerFactoryResetRule(); 53 | 54 | @Test 55 | public void read() throws Exception { 56 | Arguments args = read("input", "output"); 57 | 58 | assertEquals("Input file", "input", args.getInputFile()); 59 | assertEquals("Output file", "output", args.getOutputFile()); 60 | 61 | assertFalse("Help", args.isHelp()); 62 | assertFalse("Remove duplicates", args.isRemoveDuplicateEvents()); 63 | assertFalse("Remove Empty", args.isRemoveEmptyEvents()); 64 | assertTrue("Replace in summary", args.getReplaceInSummary().isEmpty()); 65 | assertTrue("Remove summary contains", args.getRemoveSummaryContains().isEmpty()); 66 | } 67 | 68 | @Test 69 | public void readInputOnly() throws Exception { 70 | Arguments args = read("input"); 71 | assertEquals("Input file", "input", args.getInputFile()); 72 | assertNull("Output file", args.getOutputFile()); 73 | } 74 | 75 | @Test 76 | public void readNoMainArgs() throws Exception { 77 | expectedException.expect(ArgumentException.class); 78 | expectedException.expectMessage("Main parameters"); 79 | read(""); 80 | } 81 | 82 | @Test 83 | public void readReplaceInSummary() throws Exception { 84 | Map replaceInSummary = 85 | read("--replace-summary a=b", "--replace-summary", "\"\\r(?!\\n)=\\r\\n\"", "input", "output") 86 | .getReplaceInSummary(); 87 | assertThat(replaceInSummary, hasEntry("a", "b")); 88 | assertThat(replaceInSummary, hasEntry("\\r(?!\\n)", "\\r\\n")); 89 | assertEquals("Unexpected amount of replace arguments", 2, replaceInSummary.size()); 90 | } 91 | 92 | @Test 93 | public void readReplaceInDescription() throws Exception { 94 | Map replaceInDescription = 95 | read("--replace-description a=b", "--replace-description", "\"\\r(?!\\n)=\\r\\n\"", "input", "output") 96 | .getReplaceInDescription(); 97 | assertThat(replaceInDescription, hasEntry("a", "b")); 98 | assertThat(replaceInDescription, hasEntry("\\r(?!\\n)", "\\r\\n")); 99 | assertEquals("Unexpected amount of replace arguments", 2, replaceInDescription.size()); 100 | } 101 | 102 | @Test 103 | public void readRemoveSummaryContains() throws Exception { 104 | List removeSummaryContainsMultiple = 105 | read("--remove-summary", "a", "--remove-summary", "\"b c\"", "input", "output").getRemoveSummaryContains(); 106 | List removeSummaryContainsCommaSyntax = 107 | read("--remove-summary", "\"a,b c\"", "input", "output").getRemoveSummaryContains(); 108 | assertThat(removeSummaryContainsMultiple, contains("a", "b c")); 109 | assertEquals("Unexpected amount of replace arguments", 2, removeSummaryContainsMultiple.size()); 110 | assertEquals("Multiple parameter syntax and comma syntax are not the same", removeSummaryContainsCommaSyntax, removeSummaryContainsMultiple); 111 | } 112 | 113 | @Test 114 | public void readRemoveDescriptionContains() throws Exception { 115 | List removeDescriptionContainsMultiple = 116 | read("--remove-description", "a", "--remove-description", "\"b c\"", "input", "output").getRemoveDescriptionContains(); 117 | List removeDescriptionContainsCommaSyntax = 118 | read("--remove-description", "\"a,b c\"", "input", "output").getRemoveDescriptionContains(); 119 | assertThat(removeDescriptionContainsMultiple, contains("a", "b c")); 120 | assertEquals("Unexpected amount of replace arguments", 2, removeDescriptionContainsMultiple.size()); 121 | assertEquals("Multiple parameter syntax and comma syntax are not the same", removeDescriptionContainsCommaSyntax, removeDescriptionContainsMultiple); 122 | } 123 | 124 | @Test 125 | public void readRemoveDuplicates() { 126 | Arguments read = read("--remove-duplicate-events", "input", "output"); 127 | assertTrue("Remove duplicates", read.isRemoveDuplicateEvents()); 128 | } 129 | 130 | @Test 131 | public void readRemoveEmpty() { 132 | assertTrue("Remove empty", read("--remove-empty-events", "input", "output").isRemoveEmptyEvents()); 133 | } 134 | 135 | @Test 136 | public void readHelp() throws Exception { 137 | assertTrue("Unexpected return on read()", read("input", "output", "--help").isHelp()); 138 | assertThat("Unexpected log message", getLogEvent(0).getMessage(), containsString("Usage")); 139 | assertThat("Unexpected log message", getLogEvent(0).getMessage(), containsString(PROGRAM_NAME)); 140 | } 141 | 142 | private Arguments read(String... argv) { 143 | return ArgumentsParser.read(argv, PROGRAM_NAME); 144 | } 145 | 146 | /** 147 | * @return the logging event at index. Fails if not enough logging events present. 148 | */ 149 | private LoggingEvent getLogEvent(int index) { 150 | assertThat("Unexpected number of Log messages", LOG.getLoggingEvents().size(), greaterThan(index)); 151 | return LOG.getLoggingEvents().get(index); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/ColanderIOTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.data.CalendarBuilder; 27 | import net.fortuna.ical4j.data.CalendarOutputter; 28 | import net.fortuna.ical4j.data.ParserException; 29 | import net.fortuna.ical4j.model.Calendar; 30 | import net.fortuna.ical4j.validate.ValidationException; 31 | import org.hamcrest.junit.ExpectedException; 32 | import org.junit.Rule; 33 | import org.junit.Test; 34 | import org.junit.runner.RunWith; 35 | import org.mockito.Mock; 36 | import org.mockito.invocation.InvocationOnMock; 37 | import org.mockito.junit.MockitoJUnitRunner; 38 | import org.mockito.stubbing.Answer; 39 | 40 | import java.io.ByteArrayOutputStream; 41 | import java.io.FileNotFoundException; 42 | import java.io.InputStream; 43 | import java.io.OutputStream; 44 | import java.nio.file.FileAlreadyExistsException; 45 | import java.time.LocalDateTime; 46 | import java.time.format.DateTimeFormatter; 47 | import java.util.regex.Matcher; 48 | import java.util.regex.Pattern; 49 | 50 | import static org.hamcrest.Matchers.endsWith; 51 | import static org.hamcrest.Matchers.startsWith; 52 | import static org.junit.Assert.*; 53 | import static org.mockito.ArgumentMatchers.any; 54 | import static org.mockito.Mockito.*; 55 | 56 | @RunWith(MockitoJUnitRunner.class) 57 | public class ColanderIOTest { 58 | private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(ColanderIO.DATE_TIME_FORMAT_FILE_NAME); 59 | 60 | @Rule 61 | public ExpectedException expectedException = ExpectedException.none(); 62 | @Mock 63 | private CalendarBuilder builder; 64 | @Mock 65 | private CalendarOutputter outputter; 66 | private OutputStream outStream = new ByteArrayOutputStream(); 67 | 68 | private ColanderIO io = new ColanderIOForTest(); 69 | private String outputPath; 70 | 71 | @Test 72 | public void read() throws Exception { 73 | Calendar expectedCalendar = mock(Calendar.class); 74 | InputStream stream = mock(InputStream.class); 75 | when(builder.build(stream)).thenReturn(expectedCalendar); 76 | 77 | Calendar actualCalender = io.read(stream); 78 | assertSame("Unexpected calendar returned", expectedCalendar, actualCalender); 79 | } 80 | 81 | @Test 82 | public void readException() throws Exception { 83 | ParserException expectedException = new ParserException("mocked exception message", 42); 84 | when(builder.build(any(InputStream.class))).thenThrow(expectedException); 85 | 86 | this.expectedException.expect(ColanderParserException.class); 87 | this.expectedException.expectMessage(expectedException.getMessage()); 88 | io.read(mock(InputStream.class)); 89 | } 90 | 91 | @Test 92 | public void write() throws Exception { 93 | String expectedFile = "expectedFile"; 94 | 95 | io.write(mock(Calendar.class), expectedFile, null); 96 | //calendarOutputter.output is final and cant be mocked. So assert something else 97 | assertNotEquals("write() did not write anything", 0, 98 | ((ByteArrayOutputStream) outStream).toByteArray().length); 99 | } 100 | 101 | @Test 102 | public void writeValidationException() throws Exception { 103 | String expectedFile = "expectedFile"; 104 | String expectedMessage = "mocked exception message"; 105 | outStream = mock(OutputStream.class, new ThrowValidationExceptionOnEachMethodCall(expectedMessage)); 106 | expectedException.expect(ColanderParserException.class); 107 | expectedException.expectMessage(expectedMessage); 108 | 109 | // Call method under test 110 | io.write(mock(Calendar.class), expectedFile, null); 111 | System.out.println(mockingDetails(outStream).getInvocations()); 112 | verify(outStream, atLeastOnce()).close(); 113 | } 114 | 115 | @Test 116 | public void writePathNull() throws Exception { 117 | LocalDateTime dateBefore = createComparableDateNow(LocalDateTime.now().format(formatter), formatter); 118 | io.write(mock(Calendar.class), null, "a/b.someEnding"); 119 | 120 | assertThat(outputPath, startsWith("a/b")); 121 | assertThat(outputPath, endsWith(".someEnding")); 122 | 123 | verifyDateInNewFileName(outputPath, dateBefore, "\\.someEnding"); 124 | } 125 | 126 | @Test 127 | public void writePathNullInputPathNoFileExtension() throws Exception { 128 | Calendar expectedCalendar = mock(Calendar.class); 129 | LocalDateTime dateBefore = createComparableDateNow(LocalDateTime.now().format(formatter), formatter); 130 | 131 | io.write(expectedCalendar, null, "a/b"); 132 | 133 | assertThat(outputPath, startsWith("a/b")); 134 | 135 | verifyDateInNewFileName(outputPath, dateBefore, ""); 136 | } 137 | 138 | @Test 139 | public void writeFileExists() throws Exception { 140 | expectedException.expect(FileAlreadyExistsException.class); 141 | 142 | io.write(mock(Calendar.class), 143 | // Just use this classes file to emulate an existing file 144 | createPathToClassFile(), 145 | null); 146 | } 147 | 148 | @Test 149 | public void writeFileAllArgumentsNull() throws Exception { 150 | expectedException.expect(ColanderParserException.class); 151 | expectedException.expectMessage("th input and output file paths are null"); 152 | io.write(mock(Calendar.class), null, null); 153 | } 154 | 155 | /** 156 | * Answer that makes mock throw an {@link ValidationException} on each method call. 157 | */ 158 | private static class ThrowValidationExceptionOnEachMethodCall implements Answer { 159 | String message; 160 | 161 | ThrowValidationExceptionOnEachMethodCall(String message) { 162 | this.message = message; 163 | } 164 | 165 | public Object answer(InvocationOnMock invocation) throws Throwable { 166 | throw new ValidationException(message); 167 | } 168 | } 169 | 170 | private String createPathToClassFile() { 171 | return getClass().getProtectionDomain().getCodeSource().getLocation().getPath() 172 | + getClass().getName().replace(".", "/") + ".class"; 173 | } 174 | 175 | private LocalDateTime createComparableDateNow(String format, DateTimeFormatter formatter) { 176 | return LocalDateTime.parse(format, formatter); 177 | } 178 | 179 | private void verifyDateInNewFileName(String writtenPath, LocalDateTime dateBefore, String extension) { 180 | Pattern pattern = Pattern.compile("a/b-(.*)" + extension); 181 | Matcher matcher = pattern.matcher(writtenPath); 182 | assertTrue("Date not found in new file name", matcher.find()); 183 | LocalDateTime newFileNameDate = 184 | LocalDateTime.parse(matcher.group(1), formatter); 185 | assertTrue("Date in new file name is unexpected. Expected equal or later than " + dateBefore + ", but was " + newFileNameDate, 186 | newFileNameDate.isAfter(dateBefore) || newFileNameDate.isEqual(dateBefore)); 187 | } 188 | 189 | private class ColanderIOForTest extends ColanderIO { 190 | @Override 191 | CalendarBuilder createCalenderBuilder() { return builder; } 192 | 193 | @Override 194 | CalendarOutputter createCalendarOutputter() { return outputter; } 195 | 196 | @Override 197 | OutputStream createOutputStream(String outputFile) throws FileNotFoundException { 198 | ColanderIOTest.this.outputPath = outputFile; 199 | return outStream; 200 | } 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 26 | 28 | 4.0.0 29 | 30 | info.schnatterer.colander 31 | colander-parent 32 | 0.2.1-SNAPSHOT 33 | 34 | pom 35 | 36 | colander-parent 37 | 38 | 39 | scm:git:https://github.com/schnatterer/colander 40 | scm:git:https://github.com/schnatterer/colander 41 | https://github.com/schnatterer/colander.git 42 | HEAD 43 | 44 | 45 | 46 | UTF-8 47 | UTF-8 48 | ${project.basedir} 49 | 50 | ${skipTests} 51 | 52 | 1.7.22 53 | 1.2.0 54 | 0.8.3 55 | 56 | 57 | 58 | commons-lib 59 | test-lib 60 | core 61 | cli 62 | 63 | 64 | 65 | 66 | jitpack.io 67 | https://jitpack.io 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.slf4j 75 | slf4j-api 76 | ${slf4j.version} 77 | 78 | 79 | ch.qos.logback 80 | logback-classic 81 | ${logback.version} 82 | 83 | 84 | junit 85 | junit 86 | 4.13.1 87 | test 88 | 89 | 90 | org.mockito 91 | mockito-core 92 | 2.27.0 93 | test 94 | 95 | 96 | org.hamcrest 97 | hamcrest-junit 98 | 2.0.0.0 99 | test 100 | 101 | 102 | org.assertj 103 | assertj-core 104 | 3.9.0 105 | 106 | 107 | 108 | 109 | 110 | 111 | junit 112 | junit 113 | test 114 | 115 | 116 | org.mockito 117 | mockito-core 118 | test 119 | 120 | 121 | org.hamcrest 122 | hamcrest-junit 123 | test 124 | 125 | 126 | org.assertj 127 | assertj-core 128 | test 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-assembly-plugin 138 | 3.0.0 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-compiler-plugin 143 | 3.8.1 144 | 145 | 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-compiler-plugin 151 | 152 | 11 153 | 154 | 155 | 156 | 157 | org.jacoco 158 | jacoco-maven-plugin 159 | ${jacoco.version} 160 | 161 | 162 | prepare-agent 163 | initialize 164 | 165 | prepare-agent 166 | 167 | 168 | 169 | prepare-agent-integration 170 | pre-integration-test 171 | 172 | prepare-agent-integration 173 | 174 | 175 | 176 | 177 | 178 | 179 | com.mycila 180 | license-maven-plugin 181 | 3.0 182 | 183 |

${main.basedir}/LICENSE
184 | 185 | JAVADOC_STYLE 186 | 187 | 188 | LICENSE 189 | 190 | 191 | 192 | 193 | 194 | 195 | check 196 | 197 | 198 | 199 | 200 | 201 | org.apache.maven.plugins 202 | maven-surefire-plugin 203 | 2.22.1 204 | 205 | 206 | ${skipUnitTests} 207 | 208 | 209 | 210 | org.apache.maven.plugins 211 | maven-failsafe-plugin 212 | 2.22.1 213 | 214 | 215 | 216 | integration-test 217 | verify 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /core/src/main/java/info/schnatterer/colander/Colander.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import net.fortuna.ical4j.model.Calendar; 27 | import net.fortuna.ical4j.model.Property; 28 | 29 | import java.io.IOException; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | 33 | /** 34 | * Public Interface of colander. 35 | */ 36 | public class Colander { 37 | Colander() {} 38 | 39 | /** 40 | * Puts a calender into colander. 41 | * 42 | * @param filePath path to the ical file. 43 | * @return a new instance of the {@link ColanderBuilder} 44 | */ 45 | public static ColanderBuilder toss(String filePath) { 46 | return new ColanderBuilder(filePath); 47 | } 48 | 49 | /** 50 | * Builder that allows configuring colander's filters fluently. Use {@link #rinse()} to apply. 51 | */ 52 | public static class ColanderBuilder { 53 | List filters = new ArrayList<>(); 54 | final String filePath; 55 | 56 | ColanderBuilder(String filePath) { 57 | this.filePath = filePath; 58 | } 59 | 60 | /** 61 | * Remove event when summary, description, start date or end date are the same in another event. 62 | * 63 | * @return a reference to this object. 64 | */ 65 | public ColanderBuilder removeDuplicateEvents() { 66 | filters.add(new RemoveDuplicateEventFilter()); 67 | return this; 68 | } 69 | 70 | /** 71 | * Removes event when it has 72 | *
    73 | *
  • no summary and
  • 74 | *
  • no description.
  • 75 | *
76 | * 77 | * @return a reference to this object. 78 | */ 79 | public ColanderBuilder removeEmptyEvents() { 80 | filters.add(new RemoveEmptyEventFilter()); 81 | return this; 82 | } 83 | 84 | /** 85 | * Removes a calender component, when one of its properties contains a specific string. 86 | * 87 | * @param propertyName the event property to search 88 | * @param propertyContainsString remove component when it's property contains this string 89 | * @return a reference to this object. 90 | */ 91 | public ColanderBuilder removePropertyContains(String propertyName, String propertyContainsString) { 92 | filters.add(new RemoveFilter(propertyContainsString, propertyName)); 93 | return this; 94 | } 95 | 96 | /** 97 | * Removes a calender component, when its summary contains a specific string. 98 | * 99 | * @param summaryContainsString remove when summary contains this string 100 | * @return a reference to this object. 101 | */ 102 | public ColanderBuilder removeSummaryContains(String summaryContainsString) { 103 | return removePropertyContains(Property.SUMMARY, summaryContainsString); 104 | } 105 | 106 | /** 107 | * Removes a calender component, when its summary contains a specific string. 108 | * 109 | * @param descriptionContainsString remove when summary contains this string 110 | * @return a reference to this object. 111 | */ 112 | public ColanderBuilder removeDescriptionContains(String descriptionContainsString) { 113 | return removePropertyContains(Property.DESCRIPTION, descriptionContainsString); 114 | } 115 | 116 | /** 117 | * Replaces regex in a calender component's property (e.g. summary, description, ..) 118 | * 119 | * @param propertyName property to search 120 | * @param regex regex to match 121 | * @param stringToReplaceInSummary regex to replace matching regex 122 | * @return a reference to this object. 123 | */ 124 | public ColanderBuilder replaceInProperty(String propertyName, String regex, String stringToReplaceInSummary) { 125 | filters.add(new ReplaceFilter(regex, stringToReplaceInSummary, propertyName)); 126 | return this; 127 | } 128 | 129 | /** 130 | * Replaces regex in summary of a calender component. 131 | * 132 | * @param regex regex to match 133 | * @param stringToReplaceInSummary regex to replace matching regex 134 | * @return a reference to this object. 135 | */ 136 | public ColanderBuilder replaceInSummary(String regex, String stringToReplaceInSummary) { 137 | return replaceInProperty(Property.SUMMARY, regex, stringToReplaceInSummary); 138 | } 139 | 140 | /** 141 | * Replaces regex in description of an event. 142 | * 143 | * @param regex regex to match 144 | * @param stringToReplaceInSummary regex to replace matching regex 145 | * @return a reference to this object. 146 | */ 147 | public ColanderBuilder replaceInDescription(String regex, String stringToReplaceInSummary) { 148 | return replaceInProperty(Property.DESCRIPTION, regex, stringToReplaceInSummary); 149 | } 150 | 151 | /** 152 | * Adds a custom filter to colander. 153 | * 154 | * @param filter the event filter 155 | * @return a reference to this object. 156 | */ 157 | public ColanderBuilder filter(ColanderFilter filter) { 158 | filters.add(filter); 159 | return this; 160 | } 161 | 162 | /** 163 | * Rinses colander's input, i.e. applies the filters to. 164 | * Terminates {@link ColanderBuilder} and returns a {@link ColanderResult} that allows further processing. 165 | * 166 | * @return a wrapper around the result that allows for further processing 167 | * @throws java.io.FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for 168 | * some other reason cannot be opened forreading. 169 | * @throws IOException where an error occurs reading data from the specified stream 170 | * @throws ColanderParserException where an error occurs parsing data from the stream 171 | */ 172 | public ColanderResult rinse() throws IOException { 173 | return new ColanderResult(filePath, createFilterChain().run(read(filePath))); 174 | } 175 | 176 | /** 177 | * Visible for testing. 178 | * 179 | * @return the calender at inputFilePath 180 | */ 181 | Calendar read(String filePath) throws IOException { 182 | return new ColanderIO().read(filePath); 183 | } 184 | 185 | /** 186 | * Visible for testing. 187 | * 188 | * @return a filter chain containing the configured filters. 189 | */ 190 | FilterChain createFilterChain() { 191 | return new FilterChain(filters); 192 | } 193 | 194 | 195 | } 196 | 197 | /** 198 | * Representation of rinsed calender, ready for further processing. 199 | */ 200 | public static class ColanderResult { 201 | private final Calendar result; 202 | private final String inputFilePath; 203 | 204 | ColanderResult(String inputFilePath, Calendar result) { 205 | this.inputFilePath = inputFilePath; 206 | this.result = result; 207 | } 208 | 209 | /** 210 | * Write rinsed calender to ical file 211 | * 212 | * @param outputPath the path to the ical file. When {@code null}, a new filename is generated from 213 | * {@link #inputFilePath}. 214 | * 215 | * @throws java.io.FileNotFoundException if the file exists but is a directory 216 | * rather than a regular file, does not exist but cannot 217 | * be created, or cannot be opened for any other reason 218 | * @throws IOException thrown when unable to write to output stream 219 | * @throws ColanderParserException where calendar validation fails 220 | * @throws java.nio.file.FileAlreadyExistsException if the file exists. Colander is not going to overwrite any 221 | * files. 222 | */ 223 | public void toFile(String outputPath) throws IOException { 224 | write(result, outputPath, inputFilePath); 225 | } 226 | 227 | /** 228 | * @return an in-memory-representation of rinsed calender. 229 | */ 230 | public Calendar toCalendar() { 231 | return result; 232 | } 233 | 234 | /** 235 | * Visible for testing. 236 | */ 237 | void write(Calendar result, String outputPath, String inputFilePath) throws IOException { 238 | new ColanderIO().write(result, outputPath, inputFilePath); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /core/src/test/java/info/schnatterer/colander/ColanderTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Johannes Schnatterer 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 all 14 | * 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 THE 22 | * SOFTWARE. 23 | */ 24 | package info.schnatterer.colander; 25 | 26 | import info.schnatterer.colander.Colander.ColanderBuilder; 27 | import net.fortuna.ical4j.model.Calendar; 28 | import net.fortuna.ical4j.model.Property; 29 | import net.fortuna.ical4j.model.component.VEvent; 30 | import org.junit.Test; 31 | 32 | import java.io.IOException; 33 | import java.util.Iterator; 34 | import java.util.List; 35 | import java.util.Optional; 36 | import java.util.function.Consumer; 37 | import java.util.stream.Collectors; 38 | 39 | import static org.assertj.core.api.Assertions.assertThat; 40 | import static org.junit.Assert.*; 41 | import static org.mockito.ArgumentMatchers.any; 42 | import static org.mockito.Mockito.mock; 43 | import static org.mockito.Mockito.when; 44 | 45 | public class ColanderTest { 46 | private String expectedFilePath = "file"; 47 | private FilterChain filterChain = mock(FilterChain.class); 48 | private Calendar cal = mock(Calendar.class); 49 | 50 | @Test 51 | public void toss() throws Exception { 52 | assertEquals(expectedFilePath, Colander.toss(expectedFilePath).filePath); 53 | } 54 | 55 | @Test 56 | public void removeDuplicates() throws Exception { 57 | ColanderBuilder colanderBuilder = Colander.toss(expectedFilePath).removeDuplicateEvents(); 58 | assertThat(colanderBuilder.filters).first().isOfAnyClassIn(RemoveDuplicateEventFilter.class); 59 | } 60 | 61 | @Test 62 | public void removeEmptyEvents() throws Exception { 63 | ColanderBuilder colanderBuilder = Colander.toss(expectedFilePath).removeEmptyEvents(); 64 | assertThat(colanderBuilder.filters).first().isOfAnyClassIn(RemoveEmptyEventFilter.class); 65 | } 66 | 67 | @Test 68 | public void replaceInProperty() throws Exception { 69 | String expectedProperty = "some property name"; 70 | String expectedRegex = "a"; 71 | String expectedReplacement = "b"; 72 | replaceInProperty(expectedProperty, expectedRegex, expectedReplacement, 73 | colanderBuilder ->colanderBuilder.replaceInProperty(expectedProperty, expectedRegex, expectedReplacement)); 74 | } 75 | 76 | @Test 77 | public void replaceInSummary() throws Exception { 78 | String expectedProperty = Property.SUMMARY; 79 | String expectedRegex = "a"; 80 | String expectedReplacement = "b"; 81 | replaceInProperty(expectedProperty, expectedRegex, expectedReplacement, 82 | colanderBuilder ->colanderBuilder.replaceInSummary(expectedRegex, expectedReplacement)); 83 | } 84 | 85 | @Test 86 | public void replaceInDescription() throws Exception { 87 | String expectedProperty = Property.DESCRIPTION; 88 | String expectedRegex = "a"; 89 | String expectedReplacement = "b"; 90 | replaceInProperty(expectedProperty, expectedRegex, expectedReplacement, 91 | colanderBuilder ->colanderBuilder.replaceInDescription(expectedRegex, expectedReplacement)); 92 | } 93 | 94 | @Test 95 | public void removePropertyContains() throws Exception { 96 | String expectedProperty = "some property name"; 97 | String expectedString = "str"; 98 | removePropertyContains(expectedProperty, expectedString, 99 | colanderBuilder -> colanderBuilder.removePropertyContains(expectedProperty, expectedString)); 100 | } 101 | 102 | @Test 103 | public void removeSummaryContains() throws Exception { 104 | String expectedProperty = Property.SUMMARY; 105 | String expectedString = "str"; 106 | removePropertyContains(expectedProperty, expectedString, 107 | colanderBuilder -> colanderBuilder.removeSummaryContains(expectedString)); 108 | } 109 | 110 | @Test 111 | public void removeDescriptionContains() throws Exception { 112 | String expectedProperty = Property.DESCRIPTION; 113 | String expectedString = "str"; 114 | removePropertyContains(expectedProperty, expectedString, 115 | colanderBuilder -> colanderBuilder.removeDescriptionContains(expectedString)); 116 | } 117 | 118 | @Test 119 | public void filter() throws Exception { 120 | ColanderBuilder colanderBuilder = Colander.toss(expectedFilePath).filter(event -> Optional.empty()); 121 | List allFilters = getFiltersByClass(colanderBuilder, ColanderFilter.class); 122 | assertEquals("Unexpected amount of filters found", 1, allFilters.size()); 123 | allFilters.forEach( 124 | filter -> { 125 | VEvent event = mock(VEvent.class); 126 | assertThat(filter.apply(event)).isEmpty(); 127 | } 128 | ); 129 | } 130 | 131 | @Test 132 | public void maintainsFilterOrder() { 133 | ColanderBuilder colanderBuilder = Colander.toss(expectedFilePath) 134 | .removeSummaryContains("str") 135 | .replaceInSummary("a", "b") 136 | .removeEmptyEvents() 137 | .removeDuplicateEvents(); 138 | Iterator filters = colanderBuilder.filters.iterator(); 139 | assertTrue("Unexpected order", filters.next() instanceof RemoveFilter); 140 | assertTrue("Unexpected order", filters.next() instanceof ReplaceFilter); 141 | assertTrue("Unexpected order", filters.next() instanceof RemoveEmptyEventFilter); 142 | assertTrue("Unexpected order", filters.next() instanceof RemoveDuplicateEventFilter); 143 | } 144 | 145 | 146 | @Test 147 | public void rinseToCalendar() throws Exception { 148 | ColanderBuilder builder = new ColanderBuilderForTest(expectedFilePath); 149 | 150 | when(filterChain.run(any(Calendar.class))).thenReturn(cal); 151 | 152 | assertSame(cal, builder.rinse().toCalendar()); 153 | } 154 | 155 | @Test 156 | public void rinseToFile() throws Exception { 157 | ColanderResultForTest colanderResult = new ColanderResultForTest("dontcare", cal); 158 | String expectedPath = "expectedPath"; 159 | 160 | colanderResult.toFile(expectedPath); 161 | 162 | assertSame(expectedPath, colanderResult.writtenPath); 163 | assertSame(cal, colanderResult.writtenCal); 164 | } 165 | 166 | private List getFiltersByClass(ColanderBuilder colanderBuilder, Class clazz) { 167 | return colanderBuilder.filters.stream() 168 | .filter(clazz::isInstance) 169 | .map(clazz::cast) 170 | .collect(Collectors.toList()); 171 | } 172 | 173 | private void replaceInProperty(String expectedProperty, String expectedRegex, String expectedReplacement, Consumer consumer) { 174 | ColanderBuilder colanderBuilder = Colander.toss(expectedFilePath); 175 | consumer.accept(colanderBuilder); 176 | List replaceFilters = getFiltersByClass(colanderBuilder, ReplaceFilter.class); 177 | assertEquals("Unexpected amount of filters found", 1, replaceFilters.size()); 178 | replaceFilters.forEach( 179 | filter -> { 180 | assertEquals("Unexpected property", expectedProperty, filter.getPropertyName()); 181 | assertEquals("Unexpected regex", expectedRegex, filter.getRegex()); 182 | assertEquals("Unexpected stringToReplaceInSummary", expectedReplacement, filter.getStringToReplace()); 183 | } 184 | ); 185 | } 186 | 187 | private void removePropertyContains(String expectedProperty,String expectedString, Consumer consumer) { 188 | ColanderBuilder colanderBuilder = Colander.toss(expectedFilePath); 189 | consumer.accept(colanderBuilder); 190 | List removeFilters = getFiltersByClass(colanderBuilder, RemoveFilter.class); 191 | assertEquals("Unexpected amount of filters found", 1, removeFilters.size()); 192 | removeFilters.forEach( 193 | filter -> { 194 | assertEquals("Unexpected property", expectedProperty, filter.getPropertyName()); 195 | assertEquals("Unexpected summaryContainsString", expectedString, filter.getPropertyContainsString()); 196 | } 197 | ); 198 | } 199 | 200 | private class ColanderBuilderForTest extends ColanderBuilder { 201 | ColanderBuilderForTest(String filePath) { 202 | super(filePath); 203 | } 204 | 205 | @Override 206 | Calendar read(String filePath) throws IOException { 207 | return cal; 208 | } 209 | 210 | @Override 211 | FilterChain createFilterChain() { 212 | return filterChain; 213 | } 214 | } 215 | 216 | private class ColanderResultForTest extends Colander.ColanderResult { 217 | Calendar writtenCal; 218 | String writtenPath; 219 | 220 | ColanderResultForTest(String filePath, Calendar result) { 221 | super(filePath, result); 222 | } 223 | 224 | @Override 225 | void write(Calendar result, String path, String inputPath) throws IOException { 226 | writtenCal = result; 227 | writtenPath = path; 228 | } 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /cli/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 30 | 4.0.0 31 | 32 | 33 | info.schnatterer.colander 34 | colander-parent 35 | 0.2.1-SNAPSHOT 36 | 37 | 38 | colander-cli 39 | cli 40 | 41 | jar 42 | 43 | 44 | ${project.parent.basedir} 45 | 46 | ${project.version} (commit ${buildNumber}; ${maven.build.timestamp}) 47 | 48 | 49 | 50 | 51 | ${project.parent.groupId} 52 | colander-core 53 | ${project.parent.version} 54 | 55 | 56 | 57 | ch.qos.logback 58 | logback-classic 59 | runtime 60 | 61 | 62 | 63 | com.beust 64 | jcommander 65 | 1.60 66 | 67 | 68 | 69 | com.cloudogu.versionName 70 | processor 71 | 2.1.0 72 | 73 | provided 74 | 75 | 76 | 77 | 78 | ${project.parent.groupId} 79 | colander-test-lib 80 | ${project.parent.version} 81 | test 82 | 83 | 84 | 85 | uk.org.lidalia 86 | slf4j-test 87 | 1.2.0 88 | test 89 | 90 | 91 | 92 | 93 | com.github.stefanbirkner 94 | system-rules 95 | 1.16.1 96 | test 97 | 98 | 99 | 100 | 101 | ${project.artifactId}-${project.version} 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-jar-plugin 107 | 3.0.2 108 | 109 | 110 | ${project.artifactId} 111 | 112 | 113 | true 114 | 115 | lib/ 116 | info.schnatterer.colander.cli.ColanderCli 117 | 118 | 119 | ${versionName} 120 | 121 | 122 | 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-compiler-plugin 128 | 129 | 130 | 131 | -AversionName=${versionName} 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-assembly-plugin 140 | 141 | 143 | 144 | package 145 | 146 | single 147 | 148 | 149 | 150 | 151 | 152 | org.apache.maven.plugins 153 | maven-surefire-plugin 154 | 155 | 156 | 157 | ch.qos.logback:logback-classic 158 | 159 | 160 | 161 | 162 | 163 | org.codehaus.mojo 164 | buildnumber-maven-plugin 165 | 1.4 166 | 167 | 168 | 169 | create 170 | 171 | 172 | 173 | 174 | 175 | true 176 | 177 | false 178 | 179 | false 180 | versiojn 181 | 7 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 192 | versionNameBuildNumber 193 | 194 | 195 | env.BUILD_NUMBER 196 | 197 | 198 | 199 | 200 | ${project.version} build #${env.BUILD_NUMBER} (${buildNumber}; ${maven.build.timestamp}) 201 | 202 | 203 | 204 | 205 | 207 | versionNameForRelease 208 | 209 | 210 | performRelease 211 | 212 | 213 | 214 | ${project.version} 215 | 216 | 217 | 218 | 219 | assemble-zip 220 | 221 | 222 | 223 | !jar 224 | 225 | 226 | 227 | 228 | 229 | org.apache.maven.plugins 230 | maven-assembly-plugin 231 | 232 | 233 | 234 | src/main/assembly/assembly.xml 235 | 236 | false 237 | 238 | 239 | 240 | 241 | 242 | 243 | assemble-fat-jar 244 | 245 | 246 | 247 | jar 248 | 249 | 250 | 251 | 252 | 253 | org.apache.maven.plugins 254 | maven-assembly-plugin 255 | 256 | 257 | 258 | info.schnatterer.colander.cli.ColanderCli 259 | 260 | 261 | ${versionName} 262 | 263 | 264 | 265 | jar-with-dependencies 266 | 267 | false 268 | 269 | 270 | 271 | 272 | package 273 | 274 | single 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | --------------------------------------------------------------------------------