Initial import

This commit is contained in:
Paul Campbell 2020-12-04 15:34:19 +00:00
parent 0b463863c1
commit 40b0204508
25 changed files with 875 additions and 0 deletions

41
.github/GitHub-Actions.org vendored Normal file
View file

@ -0,0 +1,41 @@
* Deploying using Github Actions
** Actions definition: workflow/sonatype-deploy.yml
When a GitHub Release is created, usually from a tag, this action will trigger.
Using JDK8 the software will be packaged, including running any tests.
Then the Deploy script will sign the created artifacts then deploy them according to the distributionManagement configuration in the `pom.xml`.
** Deploy Script
Uses a signing key provided from the GitHub Actions Secrets as an environment variable to sign the artifact(s) before they are then deployed.
*** Inputs
**** DEPLOY_PROJECTS (optional)
An optional list of modules in a multi-module project to be deployed. If this value is not specified, then all projects will be deployed.
** Maven Configuration
Picks up the credentials from Environment variables for authenticating both with GPG and with the target deployment server (e.g. sonatype-nexus).
*** Inputs
**** NEXUS_USERNAME
The username for your account on the deployment server.
**** NEXUS_PASSWORD
The password for your account on the deployement server.
**** GPG_KEYNAME
The key to use when signing.
**** GPG_PASSPHRASE
The passphrase to unlock the key to use when signing.

45
.github/NOTES vendored Normal file
View file

@ -0,0 +1,45 @@
Add subkeys:
????
Backup:
gpg --export --armor pcampbell@kemitix.net > gpg-key-backup.asc
gpg --export-secret-keys --armor pcampbell@kemitix.net >> gpg-key-backup.asc
Export sub-keys:
gpg --export-secret-subkeys pcampbell@kemitix.net > subkeys
Remove master keys:
gpg --delete-secret-key pcampbell@kemitix.net
Import sub-keys and clean up:
gpg --import subkeys
shred --remove subkeys
Delete any encryption subkeys:
gpg --edit-key pcampbell@kemitix.net
2
delkey
save
Change passphrase:
gpg --edit-key pcampbell@kemitix.net
passwd
save
Export keys:
gpg --export --armor pcampbell@kemitix.net > codesigning.asc
gpg --export-secret-keys --armor pcampbell@kemitix.net >> codesigning.asc
Encrypt keys:
gpg --symmetric --cipher-algo AES256 codesigning.asc

BIN
.github/codesigning.asc.gpg vendored Normal file

Binary file not shown.

43
.github/deploy.sh vendored Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Decrypts the signing key in .github/codesigning.asc.enc
# Imports that key
# Uses .github/settings.xml and the release profile to deploy
echo "deploy.sh: Starting..."
(
cd .github
echo "Retrieving GPG Private KEY"
gpg --quiet \
--batch \
--yes \
--decrypt \
--passphrase="${GPG_PASSPHRASE}" \
--output codesigning.asc \
codesigning.asc.gpg
echo "Loading signing key"
gpg --batch \
--fast-import codesigning.asc
)
if test -z ${DEPLOY_PROJECTS}
then
PROJECTS=""
echo "Deploying Projects: all"
else
PROJECTS="-pl ${DEPLOY_PROJECTS}"
echo "Deploying Projects: $DEPLOY_PROJECTS"
fi
echo "Releasing..."
mvn ${PROJECTS} \
--settings .github/settings.xml \
-Dskip-Tests=true \
-P release \
-B \
deploy
echo "deploy.sh: Done."

28
.github/settings.xml vendored Normal file
View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<servers>
<server>
<id>sonatype-nexus-snapshots</id>
<username>${env.NEXUS_USERNAME}</username>
<password>${env.NEXUS_PASSWORD}</password>
</server>
<server>
<id>sonatype-nexus-staging</id>
<username>${env.NEXUS_USERNAME}</username>
<password>${env.NEXUS_PASSWORD}</password>
</server>
</servers>
<profiles>
<profile>
<id>gpg-sign</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<gpg.executable>gpg</gpg.executable>
<gpg.keyname>${env.GPG_KEYNAME}</gpg.keyname>
<gpg.passphrase>${env.GPG_PASSPHRASE}</gpg.passphrase>
</properties>
</profile>
</profiles>
</settings>

22
.github/workflows/maven-build.yml vendored Normal file
View file

@ -0,0 +1,22 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Maven
run: mvn -B package --file pom.xml

24
.github/workflows/sonatype-deploy.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Deploy to Sonatype Nexus
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Maven
run: mvn -B package
- name: Nexus Repo Publish
run: sh .github/deploy.sh
env:
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

32
.gitignore vendored Normal file
View file

@ -0,0 +1,32 @@
pom.xml.versionsBackup
nohup.out
.install
target/
# Jetbrains
*.iml
.idea/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Kemitix Trello
Wrapper for Trello.
This wraps the `com.taskadapter:trello-java-wrapper` library.
It is intended for use on my `slushy` and `fuller` projects. It was originally developed as part of `slushy` and was extracted when preparing to start on `fuller` which would need some of the same functionality.

79
pom.xml Normal file
View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.kemitix</groupId>
<artifactId>kemitix-parent</artifactId>
<version>5.3.0</version>
<relativePath/>
</parent>
<artifactId>kemitix-trello</artifactId>
<version>1.0.0</version>
<properties>
<tiles-maven-plugin.version>2.18</tiles-maven-plugin.version>
<kemitix-tiles.version>2.10.0</kemitix-tiles.version>
<trello-java-wrapper.version>0.14</trello-java-wrapper.version>
<lombok.version>1.18.16</lombok.version>
<camel-api.version>3.6.0</camel-api.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.taskadapter</groupId>
<artifactId>trello-java-wrapper</artifactId>
<version>${trello-java-wrapper.version}</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-api</artifactId>
<version>${camel-api.version}</version>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>2.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>${tiles-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<tiles>
<tile>net.kemitix.tiles:maven-plugins:${kemitix-tiles.version}</tile>
<tile>net.kemitix.tiles:compiler-jdk-lts:${kemitix-tiles.version}</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,28 @@
package net.kemitix.trello;
import java.io.File;
public interface Attachment {
/**
* The name of the attachment file.
*
* @return the name of the file
*/
File getFilename();
/**
* Downlaods the file to local file system.
*
* @return the name of the local file.
*/
LocalAttachment download();
/**
* The original filename.
*
* @return the name of the file originally
*/
File getOriginalFilename();
}

View file

@ -0,0 +1,7 @@
package net.kemitix.trello;
import java.io.File;
public interface AttachmentDirectory {
File createFile(File fileName);
}

View file

@ -0,0 +1,51 @@
package net.kemitix.trello;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
@ApplicationScoped
public class AttachmentDirectoryImpl implements AttachmentDirectory {
private static final Logger LOG =
Logger.getLogger(
AttachmentDirectoryImpl.class.getName());
private Path dir;
private List<File> toDelete = new ArrayList<>();
@PostConstruct
void init() throws IOException {
dir = Files.createTempDirectory("attachments");
LOG.info("Attachments directory: " + dir);
}
@Override
public File createFile(File fileName) {
File file = dir.resolve(fileName.getName()).toFile();
LOG.info("Created attachment: " + file);
toDelete.add(file);
return file;
}
@PreDestroy
public void deleteFiles() {
toDelete.stream()
.peek(file -> LOG.info("Deleting: " + file))
.map(File::delete)
.filter(deleted -> !deleted)
.forEach(r -> LOG.warning("Could not delete file"));
if (dir.toFile().delete()) {
LOG.info("Deleted directory: " + dir);
} else {
LOG.warning("Could not delete directory: " + dir);
}
}
}

View file

@ -0,0 +1,40 @@
package net.kemitix.trello;
import com.julienvey.trello.Trello;
import com.julienvey.trello.domain.Card;
import java.util.stream.Stream;
public class CardWithAttachments {
private final com.julienvey.trello.domain.Card tcard;
private final Trello trello;
private AttachmentDirectory attachmentDir;
private CardWithAttachments(
Card tcard,
Trello trello,
AttachmentDirectory attachmentDir
) {
this.tcard = tcard;
this.trello = trello;
this.attachmentDir = attachmentDir;
}
public static CardWithAttachments create(
Card tcard,
Trello trello,
AttachmentDirectory attachmentDir
) {
return new CardWithAttachments(tcard, trello, attachmentDir);
}
public Stream<Attachment> findAttachments() {
return trello.getCardAttachments(tcard.getId()).stream()
.map(attachment -> TrelloAttachment
.create(attachment, tcard, attachmentDir));
}
public String getName() {
return tcard.getName();
}
}

View file

@ -0,0 +1,18 @@
package net.kemitix.trello;
import com.julienvey.trello.TrelloHttpClient;
import com.julienvey.trello.impl.TrelloImpl;
public class KemitixTrelloClient
extends TrelloImpl
implements TrelloClient {
public KemitixTrelloClient(
String applicationKey,
String accessToken,
TrelloHttpClient httpClient
) {
super(applicationKey, accessToken, httpClient);
}
}

View file

@ -0,0 +1,15 @@
package net.kemitix.trello;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ListUtils {
public static <A, B> List<B> map(List<A> a, Function<A, B> f) {
return a.stream()
.map(f)
.collect(Collectors.toList());
}
}

View file

@ -0,0 +1,26 @@
package net.kemitix.trello;
import org.apache.camel.Header;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.Objects;
@ApplicationScoped
public class LoadCard {
private final TrelloBoard trelloBoard;
@Inject
public LoadCard(TrelloBoard trelloBoard) {
this.trelloBoard = trelloBoard;
}
public TrelloCard loadCard(@Header("SlushyCardId") String cardId) {
return Objects.requireNonNull(
trelloBoard.getCard(cardId),
"Card Not Found"
);
}
}

View file

@ -0,0 +1,38 @@
package net.kemitix.trello;
import lombok.Getter;
import java.io.File;
/**
* An attachment that has already been downloaded.
*
* Calling download, is a noop that returns the local file.
*/
@Getter
public class LocalAttachment
implements Attachment {
private final File filename;
private final File originalFilename;
private final long length;
public LocalAttachment(
File filename,
File originalFilename,
long length
) {
this.filename = filename;
this.originalFilename = originalFilename;
this.length = length;
}
boolean isZero() {
return length == 0;
}
@Override
public LocalAttachment download() {
return this;
}
}

View file

@ -0,0 +1,87 @@
package net.kemitix.trello;
import com.julienvey.trello.domain.Card;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.util.logging.Logger;
public class TrelloAttachment implements Attachment {
private static final Logger LOG =
Logger.getLogger(
TrelloAttachment.class.getName());
private static final String[] EXTENSIONS = new String[]{"doc", "docx", "odt"};
private final com.julienvey.trello.domain.Attachment attachment;
private final Card card;
private final AttachmentDirectory attachmentDirectory;
private final String id;
private TrelloAttachment(
com.julienvey.trello.domain.Attachment attachment,
Card card,
AttachmentDirectory attachmentDirectory
) {
this.attachment = attachment;
this.card = card;
this.attachmentDirectory = attachmentDirectory;
this.id = card.getIdShort();
}
public static Attachment create(
com.julienvey.trello.domain.Attachment attachment,
Card card,
AttachmentDirectory dir
) {
return new TrelloAttachment(attachment, card, dir);
}
@Override
public File getFilename() {
return new File(String.format("%4s - %s.%s",
id, card.getName(), extension()));
}
private String extension() {
URI uri = URI.create(attachment.getUrl());
String path = uri.getPath();
for (String ex : EXTENSIONS) {
if (path.endsWith("." + ex)) {
return ex;
}
}
return "";
}
@Override
public LocalAttachment download() {
try (var source = Channels.newChannel(getUrl().openStream());){
File filename = new File(attachment.getName());
LOG.info("Downloading from " + filename);
var file = attachmentDirectory.createFile(getFilename());
LOG.info("Downloading to " + file.getCanonicalPath());
try (var channel = new FileOutputStream(file).getChannel()) {
long length = channel.transferFrom(source, 0, Long.MAX_VALUE);
LOG.info("Downloaded length: " + length);
return new LocalAttachment(file, filename, length);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public File getOriginalFilename() {
return getFilename();
}
private URL getUrl() throws MalformedURLException {
return URI.create(attachment.getUrl()).toURL();
}
}

View file

@ -0,0 +1,108 @@
package net.kemitix.trello;
import com.julienvey.trello.NotFoundException;
import com.julienvey.trello.Trello;
import com.julienvey.trello.domain.*;
import com.julienvey.trello.domain.Attachment;
import lombok.extern.java.Log;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static net.kemitix.trello.ListUtils.map;
@Log
@ApplicationScoped
public class TrelloBoard {
private final Trello trello;
private final TrelloConfig trelloConfig;
private List<TList> lists;
@Inject
public TrelloBoard(
Trello trello,
TrelloConfig trelloConfig
) {
this.trello = trello;
this.trelloConfig = trelloConfig;
}
@PostConstruct
void init () {
lists = board().fetchLists();
}
private Board board() {
String userName = trelloConfig.getUserName();
log.info("User: " + userName);
String boardName = trelloConfig.getBoardName();
log.info("Loading Board: " + boardName);
return trello
.getMemberBoards(userName)
.stream()
.filter(board -> board.getName().equals(boardName))
.findFirst()
.orElseThrow(() -> new NotFoundException("Board: " + boardName));
}
private TList getList(String listName) {
return lists
.stream()
.filter(list -> list.getName().equals(listName))
.findAny()
.orElseThrow(() -> new NotFoundException("List: " + listName));
}
public void updateCard(TrelloCard card) {
trello.updateCard(card);
}
public List<TrelloCard> getListCards(String listName) {
return trello.getListCards(getListId(listName)).stream()
.map(card -> TrelloCard.from(card, trello))
.collect(Collectors.toList());
}
public List<Attachment> getAttachments(Card card) {
return trello.getCardAttachments(card.getId());
}
public TrelloCard addMemberToCard(TrelloCard card, Member member) {
var members = trello.addMemberToCard(card.getId(), member.getId());
card.setIdMembers(map(members, Member::getId));
trello.updateCard(card);
return card;
}
public TrelloCard removeMemberFromCard(TrelloCard card, Member member) {
var members = trello.removeMemberFromCard(card.getId(), member.getId());
card.setIdMembers(map(members, Member::getId));
trello.updateCard(card);
return card;
}
public String getListId(String listName) {
return getList(listName).getId();
}
public String getBoardId() {
return board().getId();
}
public Stream<String> getListNames() {
return lists.stream()
.map(TList::getName);
}
public TrelloCard getCard(String cardId) {
Card card = trello.getCard(cardId);
return TrelloCard.from(card, trello);
}
}

View file

@ -0,0 +1,43 @@
package net.kemitix.trello;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.julienvey.trello.Trello;
import com.julienvey.trello.domain.Card;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public class TrelloCard extends Card {
private boolean dueComplete;
public static TrelloCard from(Card card, Trello trello) {
TrelloCard s = new TrelloCard();
s.setInternalTrello(trello);
s.setId(card.getId());
s.setName(card.getName());
s.setIdList(card.getIdList());
s.setDesc(card.getDesc());
s.setUrl(card.getUrl());
s.setDue(card.getDue());
s.setIdMembers(card.getIdMembers());
s.setLabels(card.getLabels());
s.setBadges(card.getBadges());
s.setCheckItemStates(card.getCheckItemStates());
s.setClosed(card.isClosed());
s.setDateLastActivity(card.getDateLastActivity());
s.setIdBoard(card.getIdBoard());
s.setIdChecklists(card.getIdChecklists());
s.setIdMembersVoted(card.getIdMembersVoted());
s.setIdShort(card.getIdShort());
s.setIdAttachmentCover(card.getIdAttachmentCover());
s.setManualCoverAttachment(card.isManualCoverAttachment());
s.setPos(card.getPos());
s.setShortLink(card.getShortLink());
s.setShortUrl(card.getShortUrl());
s.setSubscribed(card.isSubscribed());
return s;
}
}

View file

@ -0,0 +1,7 @@
package net.kemitix.trello;
import com.julienvey.trello.Trello;
public interface TrelloClient
extends Trello {
}

View file

@ -0,0 +1,48 @@
package net.kemitix.trello;
public interface TrelloConfig {
/**
* The Trello Developer API Key.
*
* <p>https://trello.com/app-key</p>
*/
String getTrelloKey();
/**
* The Trello Developer API Key Token.
*
* <p>https://trello.com/app-key</p>
*/
String getTrelloSecret();
/**
* The name of the user with access to the Trello Slush Pile Board.
*/
String getUserName();
/**
* The trello board containing the Slush Pile.
*/
String getBoardName();
/**
* The email address to send submission attachments to.
*
* <p>e.g. the kindle address</p>
*/
String getSender();
/**
* The email address to send emails from.
*
* <p>If sending to Kindle, then ensure this address is listed as a valid sender.</p>
*/
String getReader();
/**
* The URL of the Webhook to register with services.
*/
String getWebhook();
}

View file

@ -0,0 +1,38 @@
package net.kemitix.trello;
import com.julienvey.trello.Trello;
import com.julienvey.trello.TrelloHttpClient;
import com.julienvey.trello.domain.Member;
import com.julienvey.trello.impl.http.JDKTrelloHttpClient;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
@ApplicationScoped
public class TrelloProducers {
@Produces
@ApplicationScoped
TrelloHttpClient trelloHttpClient() {
return new JDKTrelloHttpClient();
}
@Produces
@ApplicationScoped
TrelloClient trello(
TrelloConfig config,
TrelloHttpClient httpClient
) {
return new KemitixTrelloClient(
config.getTrelloKey(),
config.getTrelloSecret(),
httpClient);
}
@Produces
@ApplicationScoped
Member member(Trello trello, TrelloConfig trelloConfig) {
return trello.getMemberInformation(trelloConfig.getUserName());
}
}

View file