Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(projects): refactor projects route with value objects (DEV-64) #1909

Merged
merged 5 commits into from Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -21,15 +21,14 @@ package org.knora.webapi.messages.admin.responder.projectsmessages

import java.nio.file.Path
import java.util.UUID

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.knora.webapi.IRI
import org.knora.webapi.annotation.{ApiMayChange, ServerUnique}
import org.knora.webapi.exceptions.{BadRequestException, DataConversionException, OntologyConstraintException}
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.admin.responder.usersmessages.{ProjectCreatePayloadADM, UserADM}
import org.knora.webapi.messages.admin.responder.{KnoraRequestADM, KnoraResponseADM}
import org.knora.webapi.messages.store.triplestoremessages.{StringLiteralV2, TriplestoreJsonProtocol}
import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1
Expand Down Expand Up @@ -365,13 +364,13 @@ case class ProjectDataGetRequestADM(
/**
* Requests the creation of a new project.
*
* @param createRequest the [[CreateProjectApiRequestADM]] information for creation a new project.
* @param createRequest the [[ProjectCreatePayloadADM]] information for creation a new project.
* @param featureFactoryConfig the feature factory configuration.
* @param requestingUser the user making the request.
* @param apiRequestID the ID of the API request.
*/
case class ProjectCreateRequestADM(
createRequest: CreateProjectApiRequestADM,
createRequest: ProjectCreatePayloadADM,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM,
apiRequestID: UUID
Expand Down
@@ -0,0 +1,59 @@
package org.knora.webapi.messages.admin.responder.usersmessages

import org.knora.webapi.IRI

trait ProjectCreatePayloadTraitADM {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trait is superfluous here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call it something else

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case you don't even need it, just delete it

def create(
id: Option[IRI],
shortname: Shortname,
shortcode: Shortcode,
longname: Option[Longname],
description: Description,
keywords: Keywords,
logo: Option[Logo],
status: Status,
selfjoin: Selfjoin
): ProjectCreatePayloadADM
}

/**
* Project payload
*/
sealed abstract case class ProjectCreatePayloadADM(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@irinaschubert I found the example for smart constructors. There is a private keyword missing:

sealed abstract case class ProjectCreatePayloadADM private (

id: Option[IRI],
shortname: Shortname,
shortcode: Shortcode,
longname: Option[Longname],
description: Description,
keywords: Keywords,
logo: Option[Logo],
status: Status,
selfjoin: Selfjoin
)

object ProjectCreatePayloadADM extends ProjectCreatePayloadTraitADM {

/** The create constructor */
override def create(
id: Option[IRI] = None,
shortname: Shortname,
shortcode: Shortcode,
longname: Option[Longname] = None,
description: Description,
keywords: Keywords,
logo: Option[Logo] = None,
status: Status,
selfjoin: Selfjoin
): ProjectCreatePayloadADM =
new ProjectCreatePayloadADM(
id = id,
shortname = shortname,
shortcode = shortcode,
longname = longname,
description = description,
keywords = keywords,
logo = logo,
status = status,
selfjoin = selfjoin
) {}
}
Expand Up @@ -2,13 +2,6 @@ package org.knora.webapi.messages.admin.responder.usersmessages

import org.knora.webapi.IRI

sealed trait ValidationError
case object InvalidUsername extends ValidationError
case object InvalidEmail extends ValidationError
case object InvalidGivenOrFamilyName extends ValidationError
case object InvalidPassword extends ValidationError
case object InvalidLanguageCode extends ValidationError

trait UserCreatePayloadTraitADM {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, trait is not needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a trait can be seen as a kind of an interface. you don't need an interface here, at is only used here and nowhere else.

def create(
id: Option[IRI],
Expand Down
@@ -1,11 +1,14 @@
package org.knora.webapi.messages.admin.responder.usersmessages

import org.knora.webapi.LanguageCodes
import org.knora.webapi.exceptions.BadRequestException
import org.knora.webapi.exceptions.{AssertionException, BadRequestException}
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2

import scala.util.matching.Regex

trait ValueObject[T, K] {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the ValueObject trait. It brings hardly any benefits, but makes the implementations look more complicated than necessary. Don't over engineer things. Please keep them simple.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean by this, no trait at all here?

val stringFormatter = StringFormatter.getGeneralInstance
def create(value: K): Either[Throwable, T]
}

Expand Down Expand Up @@ -144,3 +147,105 @@ object SystemAdmin extends ValueObject[SystemAdmin, Boolean] {
override def create(value: Boolean): Either[Throwable, SystemAdmin] =
Right(new SystemAdmin(value) {})
}

/**
* Project shortcode value object.
*/
sealed abstract case class Shortcode(value: String)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing private keyword


object Shortcode extends ValueObject[Shortcode, String] {

override def create(value: String): Either[Throwable, Shortcode] =
if (value.isEmpty) {
Left(BadRequestException("Missing shortcode"))
} else {
val shortcode: String = stringFormatter.validateProjectShortcode(value, throw AssertionException("not valid"))
Right(new Shortcode(shortcode) {})
}
}

/**
* Project shortname value object.
*/
sealed abstract case class Shortname(value: String)

object Shortname extends ValueObject[Shortname, String] {

override def create(value: String): Either[Throwable, Shortname] =
if (value.isEmpty) {
Left(BadRequestException("Missing shortname"))
} else {
val shortname = stringFormatter.validateAndEscapeProjectShortname(value, throw AssertionException("not valid"))
Right(new Shortname(shortname) {})
}
}

/**
* Project longname value object.
*/
sealed abstract case class Longname(value: String)

object Longname extends ValueObject[Longname, String] {

override def create(value: String): Either[Throwable, Longname] =
if (value.isEmpty) {
Left(BadRequestException("Missing long name"))
} else {
Right(new Longname(value) {})
}
}

/**
* Project description value object.
*/
sealed abstract case class Description(value: Seq[StringLiteralV2])

object Description extends ValueObject[Description, Seq[StringLiteralV2]] {

override def create(value: Seq[StringLiteralV2]): Either[Throwable, Description] =
if (value.isEmpty) {
Left(BadRequestException("Missing description"))
} else {
Right(new Description(value) {})
}
}

/**
* Project keywords value object.
*/
sealed abstract case class Keywords(value: Seq[String])

object Keywords extends ValueObject[Keywords, Seq[String]] {

override def create(value: Seq[String]): Either[Throwable, Keywords] =
if (value.isEmpty) {
Left(BadRequestException("Missing keywords"))
} else {
Right(new Keywords(value) {})
}
}

/**
* Project logo value object.
*/
sealed abstract case class Logo(value: String)

object Logo extends ValueObject[Logo, String] {

override def create(value: String): Either[Throwable, Logo] =
if (value.isEmpty) {
Left(BadRequestException("Missing logo"))
} else {
Right(new Logo(value) {})
}
}

/**
* selfjoin value object.
*/
sealed abstract case class Selfjoin(value: Boolean)

object Selfjoin extends ValueObject[Selfjoin, Boolean] {
override def create(value: Boolean): Either[Throwable, Selfjoin] =
Right(new Selfjoin(value) {})
}
Expand Up @@ -22,7 +22,6 @@ package org.knora.webapi.responders.admin
import java.io.{BufferedInputStream, BufferedOutputStream}
import java.nio.file.{Files, Path}
import java.util.UUID

import akka.http.scaladsl.util.FastFuture
import akka.pattern._
import org.knora.webapi._
Expand All @@ -33,13 +32,13 @@ import org.knora.webapi.instrumentation.InstrumentationSupport
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.admin.responder.projectsmessages._
import org.knora.webapi.messages.admin.responder.usersmessages.{
ProjectCreatePayloadADM,
UserADM,
UserGetADM,
UserIdentifierADM,
UserInformationTypeADM
}
import org.knora.webapi.messages.admin.responder.permissionsmessages._

import org.knora.webapi.messages.store.cacheservicemessages.{
CacheServiceGetProjectADM,
CacheServicePutProjectADM,
Expand Down Expand Up @@ -1004,7 +1003,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
* @throws BadRequestException in the case when the shortcode is invalid.
*/
private def projectCreateRequestADM(
createProjectRequest: CreateProjectApiRequestADM,
createProjectRequest: ProjectCreatePayloadADM,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM,
apiRequestID: UUID
Expand Down Expand Up @@ -1084,25 +1083,25 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
} yield ()

def projectCreateTask(
createProjectRequest: CreateProjectApiRequestADM,
createProjectRequest: ProjectCreatePayloadADM,
requestingUser: UserADM
): Future[ProjectOperationResponseADM] =
for {

// check if the supplied shortname is unique
shortnameExists <- projectByShortnameExists(createProjectRequest.shortname)
shortnameExists <- projectByShortnameExists(createProjectRequest.shortname.value)
_ = if (shortnameExists) {
throw DuplicateValueException(
s"Project with the shortname: '${createProjectRequest.shortname}' already exists"
s"Project with the shortname: '${createProjectRequest.shortname.value}' already exists"
)
}

// check if the optionally supplied shortcode is valid and unique
shortcodeExists <- projectByShortcodeExists(createProjectRequest.shortcode)
shortcodeExists <- projectByShortcodeExists(createProjectRequest.shortcode.value)

_ = if (shortcodeExists) {
throw DuplicateValueException(
s"Project with the shortcode: '${createProjectRequest.shortcode}' already exists"
s"Project with the shortcode: '${createProjectRequest.shortcode.value}' already exists"
)
}

Expand All @@ -1116,27 +1115,37 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
customProjectIri: Option[SmartIri] = createProjectRequest.id.map(iri => iri.toSmartIri)
newProjectIRI: IRI <- checkOrCreateEntityIri(
customProjectIri,
stringFormatter.makeRandomProjectIri(createProjectRequest.shortcode)
stringFormatter.makeRandomProjectIri(createProjectRequest.shortcode.value)
)

maybeLongname = createProjectRequest.longname match {
case Some(value) => Some(value.value)
case None => None
}

maybeLogo = createProjectRequest.logo match {
case Some(value) => Some(value.value)
case None => None
}

createNewProjectSparqlString = org.knora.webapi.messages.twirl.queries.sparql.admin.txt
.createNewProject(
adminNamedGraphIri = OntologyConstants.NamedGraphs.AdminNamedGraph,
triplestore = settings.triplestoreType,
projectIri = newProjectIRI,
projectClassIri = OntologyConstants.KnoraAdmin.KnoraProject,
shortname = createProjectRequest.shortname,
shortcode = createProjectRequest.shortcode,
maybeLongname = createProjectRequest.longname,
maybeDescriptions = if (createProjectRequest.description.nonEmpty) {
Some(createProjectRequest.description)
shortname = createProjectRequest.shortname.value,
shortcode = createProjectRequest.shortcode.value,
maybeLongname = maybeLongname,
maybeDescriptions = if (createProjectRequest.description.value.nonEmpty) {
Some(createProjectRequest.description.value)
} else None,
maybeKeywords = if (createProjectRequest.keywords.nonEmpty) {
Some(createProjectRequest.keywords)
maybeKeywords = if (createProjectRequest.keywords.value.nonEmpty) {
Some(createProjectRequest.keywords.value)
} else None,
maybeLogo = createProjectRequest.logo,
status = createProjectRequest.status,
hasSelfJoinEnabled = createProjectRequest.selfjoin
maybeLogo = maybeLogo,
status = createProjectRequest.status.value,
hasSelfJoinEnabled = createProjectRequest.selfjoin.value
)
.toString

Expand Down