Aggregation: Aggregate top level collections

Adds aggregation support for all top level collections which do not
follow the usual form of /redfish/v1/<collection>.

As part of this change we no longer forward all requests that fit the
above format such as /redfish/v1/UpdateService.  We now skip the
forwarding rather than sending a request.  Previously we forwarded all
potential collection requests and then relied on the lack of a
"Members" array in the response to denote that the request was not
actually for a valid collection.

Tested:
We aggregate or try to aggregate these URIs as collections:
/redfish/v1/Chassis
/redfish/v1/Chassis/
/redfish/v1/Fabrics
/redfish/v1/Fabrics/
/redfish/v1/TelemetryService/MetricReports
/redfish/v1/TelemetryService/MetricReports/

We aggregate or try to aggregate these URIs as satellite resources
/redfish/v1/Chassis/5B247A_<chassisID>
/redfish/v1/TelemetryService/MetricReports/5B247A_reportID
/redfish/v1/UpdateService/FirmwareInventory/5B247A_firmwareID

We do not attempt to aggregate these URIs at all:
/redfish/v1/Fake
/redfish/v1/Fake/
/redfish/v1/Fake/5B247A_fakeID
/redfish/v1/TelemetryService
/redfish/v1/TelemetryService/
/redfish/v1/TelemetryService/5B247A_fakeID

Signed-off-by: Carson Labrado <clabrado@google.com>
Change-Id: I133503ab3e4df7777df094768786e511880ca70f
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index b3252a3..be5c8aa 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -492,25 +492,28 @@
             return;
         }
 
-        std::string updateServiceName;
-        std::string memberName;
-        if (crow::utility::readUrlSegments(
-                thisReq.urlView, "redfish", "v1", "UpdateService",
-                std::ref(updateServiceName), std::ref(memberName),
-                crow::utility::OrMorePaths()))
-        {
-            // Must be FirmwareInventory or SoftwareInventory
-            findSatellite(thisReq, asyncResp, satelliteInfo, memberName);
-            return;
-        }
+        const boost::urls::segments_view urlSegments =
+            thisReq.urlView.segments();
+        boost::urls::url currentUrl("/");
+        boost::urls::segments_view::iterator it = urlSegments.begin();
+        const boost::urls::segments_view::const_iterator end =
+            urlSegments.end();
 
-        std::string collectionName;
-        if (crow::utility::readUrlSegments(
-                thisReq.urlView, "redfish", "v1", std::ref(collectionName),
-                std::ref(memberName), crow::utility::OrMorePaths()))
+        // Skip past the leading "/redfish/v1"
+        it++;
+        it++;
+        for (; it != end; it++)
         {
-            findSatellite(thisReq, asyncResp, satelliteInfo, memberName);
-            return;
+            if (std::binary_search(topCollections.begin(), topCollections.end(),
+                                   currentUrl.buffer()))
+            {
+                // We've matched a resource collection so this current segment
+                // must contain an aggregation prefix
+                findSatellite(thisReq, asyncResp, satelliteInfo, *it);
+                return;
+            }
+
+            currentUrl.segments().push_back(*it);
         }
 
         // We shouldn't reach this point since we should've hit one of the
@@ -770,11 +773,6 @@
         using crow::utility::OrMorePaths;
         using crow::utility::readUrlSegments;
         const boost::urls::url_view url = thisReq.urlView;
-        // UpdateService is the only top level resource that is not a Collection
-        if (readUrlSegments(url, "redfish", "v1", "UpdateService"))
-        {
-            return Result::LocalHandle;
-        }
 
         // We don't need to aggregate JsonSchemas due to potential issues such
         // as version mismatches between aggregator and satellite BMCs.  For
@@ -786,52 +784,72 @@
             return Result::LocalHandle;
         }
 
-        if (readUrlSegments(url, "redfish", "v1", "UpdateService",
-                            "SoftwareInventory") ||
-            readUrlSegments(url, "redfish", "v1", "UpdateService",
-                            "FirmwareInventory"))
+        // The first two segments should be "/redfish/v1".  We need to check
+        // that before we can search topCollections
+        if (!crow::utility::readUrlSegments(url, "redfish", "v1",
+                                            crow::utility::OrMorePaths()))
         {
-            startAggregation(AggregationType::Collection, thisReq, asyncResp);
             return Result::LocalHandle;
         }
 
-        // Is the request for a resource collection?:
-        // /redfish/v1/<resource>
-        // e.g. /redfish/v1/Chassis
-        std::string collectionName;
-        if (readUrlSegments(url, "redfish", "v1", std::ref(collectionName)))
-        {
-            startAggregation(AggregationType::Collection, thisReq, asyncResp);
-            return Result::LocalHandle;
-        }
+        // Parse the URI to see if it begins with a known top level collection
+        // such as:
+        // /redfish/v1/Chassis
+        // /redfish/v1/UpdateService/FirmwareInventory
+        const boost::urls::segments_view urlSegments = url.segments();
+        std::string collectionItem;
+        boost::urls::url currentUrl("/");
+        boost::urls::segments_view::iterator it = urlSegments.begin();
+        const boost::urls::segments_view::const_iterator end =
+            urlSegments.end();
 
-        // We know that the ID of an aggregated resource will begin with
-        // "5B247A".  For the most part the URI will begin like this:
-        // /redfish/v1/<resource>/<resource ID>
-        // Note, FirmwareInventory and SoftwareInventory are "special" because
-        // they are two levels deep, but still need aggregated
-        // /redfish/v1/UpdateService/FirmwareInventory/<FirmwareInventory ID>
-        // /redfish/v1/UpdateService/SoftwareInventory/<SoftwareInventory ID>
-        std::string memberName;
-        if (readUrlSegments(url, "redfish", "v1", "UpdateService",
-                            "SoftwareInventory", std::ref(memberName),
-                            OrMorePaths()) ||
-            readUrlSegments(url, "redfish", "v1", "UpdateService",
-                            "FirmwareInventory", std::ref(memberName),
-                            OrMorePaths()) ||
-            readUrlSegments(url, "redfish", "v1", std::ref(collectionName),
-                            std::ref(memberName), OrMorePaths()))
+        // Skip past the leading "/redfish/v1"
+        it++;
+        it++;
+        for (; it != end; it++)
         {
-            if (memberName.starts_with("5B247A"))
+            collectionItem = *it;
+            if (std::binary_search(topCollections.begin(), topCollections.end(),
+                                   currentUrl.buffer()))
             {
-                BMCWEB_LOG_DEBUG << "Need to forward a request";
+                // We've matched a resource collection so this current segment
+                // might contain an aggregation prefix
+                if (collectionItem.starts_with("5B247A"))
+                {
+                    BMCWEB_LOG_DEBUG << "Need to forward a request";
 
-                // Extract the prefix from the request's URI, retrieve the
-                // associated satellite config information, and then forward the
-                // request to that satellite.
-                startAggregation(AggregationType::Resource, thisReq, asyncResp);
-                return Result::NoLocalHandle;
+                    // Extract the prefix from the request's URI, retrieve the
+                    // associated satellite config information, and then forward
+                    // the request to that satellite.
+                    startAggregation(AggregationType::Resource, thisReq,
+                                     asyncResp);
+                    return Result::NoLocalHandle;
+                }
+
+                // Handle collection URI with a trailing backslash
+                // e.g. /redfish/v1/Chassis/
+                it++;
+                if ((it == end) && collectionItem.empty())
+                {
+                    startAggregation(AggregationType::Collection, thisReq,
+                                     asyncResp);
+                }
+
+                // We didn't recognize the prefix or it's a collection with a
+                // trailing "/".  In both cases we still want to locally handle
+                // the request
+                return Result::LocalHandle;
             }
+
+            currentUrl.segments().push_back(collectionItem);
+        }
+
+        // If we made it here then currentUrl could contain a top level
+        // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
+        if (std::binary_search(topCollections.begin(), topCollections.end(),
+                               currentUrl.buffer()))
+        {
+            startAggregation(AggregationType::Collection, thisReq, asyncResp);
             return Result::LocalHandle;
         }