diff --git a/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java b/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java index a293b62bd..14e8ef568 100644 --- a/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java +++ b/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java @@ -24,6 +24,13 @@ public abstract class HttpHeaderRange { public abstract boolean matches(String header); + /** + * Produces a new range that matches the headers of this range and the given range. + * + * @since 2.0.0 + */ + public abstract HttpHeaderRange concat(HttpHeaderRange range); + public static HttpHeaderRange create(String... headers) { return HttpHeaderRange$.MODULE$.apply(Util.convertArray(headers)); } diff --git a/http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes b/http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes new file mode 100644 index 000000000..5a5786bc2 --- /dev/null +++ b/http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# New methods added in 2.0.x +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.cors.javadsl.model.HttpHeaderRange.concat") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.cors.scaladsl.model.HttpHeaderRange.concat") diff --git a/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala b/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala index e9ef33e45..e451ff6da 100644 --- a/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala +++ b/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala @@ -27,5 +27,8 @@ private[pekko] object CorsJavaMapping { object Implicits { implicit object CorsSettingsMapping extends JavaMapping.Inherited[javadsl.settings.CorsSettings, scaladsl.settings.CorsSettings] + + implicit object HttpHeaderRangeMapping + extends JavaMapping.Inherited[javadsl.model.HttpHeaderRange, scaladsl.model.HttpHeaderRange] } } diff --git a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala index 454a14cc7..fad2930d6 100644 --- a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala +++ b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala @@ -20,19 +20,39 @@ package org.apache.pekko.http.cors.scaladsl.model import scala.collection.immutable.Seq import org.apache.pekko +import pekko.http.cors.CorsJavaMapping.Implicits._ import pekko.http.cors.javadsl +import pekko.http.impl.util.JavaMapping import pekko.util.Helpers -abstract class HttpHeaderRange extends javadsl.model.HttpHeaderRange +sealed abstract class HttpHeaderRange extends javadsl.model.HttpHeaderRange { + override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange + + /** + * Operator alias for [[concat]]. + * + * @since 2.0.0 + */ + def ++(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = concat(range) +} object HttpHeaderRange { case object `*` extends HttpHeaderRange { def matches(header: String) = true + + override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = this } final case class Default(headers: Seq[String]) extends HttpHeaderRange { private val lowercaseHeaders: Seq[String] = headers.map(Helpers.toRootLowerCase) def matches(header: String): Boolean = lowercaseHeaders.contains(Helpers.toRootLowerCase(header)) + + override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = { + JavaMapping.toScala(range) match { + case `*` => `*` + case Default(headers) => Default(this.headers ++ headers) + } + } } def apply(headers: String*): Default = Default(Seq(headers: _*)) diff --git a/http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala b/http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala new file mode 100644 index 000000000..55d68ac04 --- /dev/null +++ b/http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.pekko.http.cors.scaladsl.model + +import org.apache.pekko.http.scaladsl.model.headers.{ `Content-Type`, Accept } +import org.scalatest.Inspectors +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class HttpHeaderRangeSpec extends AnyWordSpec with Matchers with Inspectors { + + "The `*` range" should { + "match any Header" in { + val headers = Seq( + `Content-Type`.name, + "conTent-tyPe", + Accept.name, + "x-any-random-header-name") + + forAll(headers) { o => HttpHeaderRange.*.matches(o) shouldBe true } + } + + "be printed as `*`" in { + HttpHeaderRange.*.toString shouldBe "*" + } + } + + "The default range" should { + val range = HttpHeaderRange(`Content-Type`.name, Accept.name) + + "match headers ignoring case" in { + val headers = Seq( + `Content-Type`.name, + `Content-Type`.name.toUpperCase(), + `Content-Type`.name.toLowerCase(), + "conTent-tyPe", + Accept.name, + Accept.name.toUpperCase(), + Accept.name.toLowerCase(), + "aCcepT") + + forAll(headers) { o => range.matches(o) shouldBe true } + } + + "not match other headers" in { + val headers = Seq( + "Content-Type2", + "Content-Typ", + "x-any-random-header-name") + + forAll(headers) { o => range.matches(o) shouldBe false } + } + } + + "Concatenation of ranges" should { + "match both ranges" in { + val range1 = HttpHeaderRange(`Content-Type`.name) + val range2 = HttpHeaderRange(Accept.name) + + val combined = range1 ++ range2 + + val headers = Seq( + `Content-Type`.name, + Accept.name) + val notHeaders = Seq( + "Content-Type2", + "Content-Typ", + "x-any-random-header-name") + + forAll(headers) { o => combined.matches(o) shouldBe true } + forAll(notHeaders) { o => combined.matches(o) shouldBe false } + } + + "combine with the `*` range" in { + val range1 = HttpHeaderRange(`Content-Type`.name) + val starRange = HttpHeaderRange.* + + val combinedBefore = starRange ++ range1 + val combinedAfter = range1 ++ starRange + + val headers = Seq( + `Content-Type`.name, + "conTent-tyPe", + Accept.name, + "x-any-random-header-name") + + forAll(headers) { o => combinedBefore.matches(o) shouldBe true } + forAll(headers) { o => combinedAfter.matches(o) shouldBe true } + } + } + +}