Java tutorial
/* * Copyright 2014 Google Inc. All Rights Reserved. * * Licensed 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 com.google.doubleclick.openrtb; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.Math.max; import static java.lang.Math.min; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; import com.google.doubleclick.DcExt; import com.google.doubleclick.crypto.DoubleClickCrypto; import com.google.doubleclick.util.CityDMARegionKey; import com.google.doubleclick.util.CityDMARegionValue; import com.google.doubleclick.util.CountryCodes; import com.google.doubleclick.util.DoubleClickMetadata; import com.google.doubleclick.util.GeoTarget; import com.google.openrtb.OpenRtb; import com.google.openrtb.OpenRtb.BidRequest.App; import com.google.openrtb.OpenRtb.BidRequest.AuctionType; import com.google.openrtb.OpenRtb.BidRequest.Content; import com.google.openrtb.OpenRtb.BidRequest.Content.Builder; import com.google.openrtb.OpenRtb.BidRequest.Data; import com.google.openrtb.OpenRtb.BidRequest.Data.Segment; import com.google.openrtb.OpenRtb.BidRequest.Device; import com.google.openrtb.OpenRtb.BidRequest.Device.DeviceType; import com.google.openrtb.OpenRtb.BidRequest.Geo; import com.google.openrtb.OpenRtb.BidRequest.Imp; import com.google.openrtb.OpenRtb.BidRequest.Imp.APIFramework; import com.google.openrtb.OpenRtb.BidRequest.Imp.AdPosition; import com.google.openrtb.OpenRtb.BidRequest.Imp.Banner; import com.google.openrtb.OpenRtb.BidRequest.Imp.Native; import com.google.openrtb.OpenRtb.BidRequest.Imp.Pmp; import com.google.openrtb.OpenRtb.BidRequest.Imp.Pmp.Deal; import com.google.openrtb.OpenRtb.BidRequest.Imp.Video; import com.google.openrtb.OpenRtb.BidRequest.Imp.Video.VASTCompanionType; import com.google.openrtb.OpenRtb.BidRequest.Imp.Video.VideoBidResponseProtocol; import com.google.openrtb.OpenRtb.BidRequest.Publisher; import com.google.openrtb.OpenRtb.BidRequest.Regs; import com.google.openrtb.OpenRtb.BidRequest.Site; import com.google.openrtb.OpenRtb.BidRequest.User; import com.google.openrtb.OpenRtb.BidResponse.SeatBid; import com.google.openrtb.OpenRtb.BidResponse.SeatBid.Bid; import com.google.openrtb.OpenRtb.ContentCategory; import com.google.openrtb.json.OpenRtbJsonFactory; import com.google.openrtb.mapper.OpenRtbMapper; import com.google.openrtb.util.OpenRtbUtils; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.TextFormat; import com.google.protos.adx.NetworkBid; import com.codahale.metrics.Counter; import com.codahale.metrics.MetricRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.SignatureException; import java.util.Calendar; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; /** * Mapping between the DoubleClick and OpenRTB models. * <p> * This class is threadsafe. Recommended use is as a singleton, but you may also want to create * multiple instances if you need to keep track of metrics separately for different uses * (for that to make sense, provide a different {@link MetricRegistry} to each instance). */ @Singleton public class DoubleClickOpenRtbMapper implements OpenRtbMapper<NetworkBid.BidRequest, NetworkBid.BidResponse, NetworkBid.BidRequest.Builder, NetworkBid.BidResponse.Builder> { private static final Logger logger = LoggerFactory.getLogger(DoubleClickOpenRtbMapper.class); private static final String YOUTUBE_AFV_USER_ID = "afv_user_id_"; private static final Pattern SEMITRANSPARENT_CHANNEL = Pattern .compile("pack-(brand|semi|anon)-([^\\-]+)::(.+)"); private static final Joiner csvJoiner = Joiner.on(",").skipNulls(); private static final int MICROS_PER_CURRENCY_UNIT = 1_000_000; private final DoubleClickMetadata metadata; private final DoubleClickCrypto.Hyperlocal hyperlocalCrypto; private final ImmutableList<ExtMapper> extMappers; private final DoubleClickOpenRtbNativeMapper nativeMapper; private final Counter missingCrid = new Counter(); private final Counter invalidImp = new Counter(); private final Counter missingSize = new Counter(); private final Counter noVideoOrBanner = new Counter(); private final Counter coppaTreatment = new Counter(); private final Counter noImp = new Counter(); private final Counter invalidGeoId = new Counter(); private final Counter invalidHyperlocal = new Counter(); private final Counter noCid = new Counter(); private final Counter invalidContentCategory = new Counter(); @Inject public DoubleClickOpenRtbMapper(MetricRegistry metricRegistry, DoubleClickMetadata metadata, @Nullable OpenRtbJsonFactory jsonFactory, @Nullable DoubleClickCrypto.Hyperlocal hyperlocalCrypto, List<ExtMapper> extMappers) { this.metadata = metadata; this.hyperlocalCrypto = hyperlocalCrypto; this.extMappers = ImmutableList.copyOf(extMappers); this.nativeMapper = new DoubleClickOpenRtbNativeMapper(metricRegistry, jsonFactory, extMappers); Class<? extends DoubleClickOpenRtbMapper> cls = getClass(); metricRegistry.register(MetricRegistry.name(cls, "missing-crid"), missingCrid); metricRegistry.register(MetricRegistry.name(cls, "invalid-imp"), invalidImp); metricRegistry.register(MetricRegistry.name(cls, "missing-size"), missingSize); metricRegistry.register(MetricRegistry.name(cls, "no-video-or-banner"), noVideoOrBanner); metricRegistry.register(MetricRegistry.name(cls, "coppa-treatment"), coppaTreatment); metricRegistry.register(MetricRegistry.name(cls, "no-imp"), noImp); metricRegistry.register(MetricRegistry.name(cls, "invalid-geoid"), invalidGeoId); metricRegistry.register(MetricRegistry.name(cls, "invalid-hyperlocal"), invalidHyperlocal); metricRegistry.register(MetricRegistry.name(cls, "no-cid"), noCid); metricRegistry.register(MetricRegistry.name(cls, "invalid-cat"), invalidContentCategory); } @Override public OpenRtb.BidRequest.Builder toOpenRtbBidRequest(NetworkBid.BidRequest dcRequest) { OpenRtb.BidRequest.Builder request = OpenRtb.BidRequest.newBuilder() .setId(BaseEncoding.base64Url().omitPadding().encode(dcRequest.getId().toByteArray())); if (dcRequest.getIsPing()) { return request; } boolean coppa = false; for (NetworkBid.BidRequest.UserDataTreatment dcUDT : dcRequest.getUserDataTreatmentList()) { if (dcUDT == NetworkBid.BidRequest.UserDataTreatment.TAG_FOR_CHILD_DIRECTED_TREATMENT) { coppa = true; break; } } if (coppa) { coppaTreatment.inc(); request.setRegs(Regs.newBuilder().setCoppa(true)); } request.setDevice(buildDevice(dcRequest, coppa)); if (dcRequest.hasMobile()) { App.Builder app = buildApp(dcRequest); if (app != null) { request.setApp(app); } } else { Site.Builder site = buildSite(dcRequest); if (site != null) { request.setSite(site); } } EnumSet<ContentCategory> cats = EnumSet.noneOf(ContentCategory.class); for (NetworkBid.BidRequest.AdSlot dcSlot : dcRequest.getAdslotList()) { Imp.Builder imp = buildImp(dcRequest, dcSlot); if (imp != null) { request.addImp(imp); AdCategoryMapper.toOpenRtb(dcSlot.getExcludedProductCategoryList(), cats); AdCategoryMapper.toOpenRtb(dcSlot.getExcludedSensitiveCategoryList(), cats); } } for (ContentCategory cat : cats) { request.addBcat(OpenRtbUtils.categoryToJsonName(cat.name())); } if (request.getImpCount() == 0) { noImp.inc(); logger.debug("Request has no impressions"); } User.Builder user = buildUser(dcRequest, coppa); if (user != null) { request.setUser(user); } if (dcRequest.hasIsTest()) { request.setTest(dcRequest.getIsTest()); } request.setTmax(100); for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbBidRequest(dcRequest, request); } return request; } protected Device.Builder buildDevice(NetworkBid.BidRequest dcRequest, boolean coppa) { Device.Builder device = Device.newBuilder(); if (dcRequest.hasIp()) { if (dcRequest.getIp().size() <= 4) { device.setIp(MapperUtil.toIpv4String(dcRequest.getIp())); } else { device.setIpv6(MapperUtil.toIpv6String(dcRequest.getIp())); } } if (dcRequest.hasUserAgent()) { device.setUa(dcRequest.getUserAgent()); } Geo.Builder geo = buildGeo(dcRequest); if (geo != null) { device.setGeo(geo); } if (dcRequest.hasMobile()) { NetworkBid.BidRequest.Mobile dcMobile = dcRequest.getMobile(); if ((coppa && dcMobile.hasConstrainedUsageEncryptedAdvertisingId()) || (!coppa && dcMobile.hasEncryptedAdvertisingId())) { device.setIfa( BaseEncoding.base16().encode((coppa ? dcMobile.getConstrainedUsageEncryptedAdvertisingId() : dcMobile.getEncryptedAdvertisingId()).toByteArray())); device.setLmt(false); } else if ((coppa && dcMobile.hasConstrainedUsageEncryptedHashedIdfa()) || (!coppa && dcMobile.hasEncryptedHashedIdfa())) { device.setDpidmd5( BaseEncoding.base16().encode((coppa ? dcMobile.getConstrainedUsageEncryptedHashedIdfa() : dcMobile.getEncryptedHashedIdfa()).toByteArray())); device.setLmt(false); } else { device.setLmt(true); } if (dcMobile.hasCarrierId()) { device.setCarrier(String.valueOf(dcMobile.getCarrierId())); } if (dcMobile.hasModel()) { device.setModel(dcMobile.getModel()); } if (dcMobile.hasPlatform()) { device.setOs(dcMobile.getPlatform()); } if (dcMobile.getOsVersion().hasOsVersionMajor()) { NetworkBid.BidRequest.Mobile.DeviceOsVersion dcVer = dcMobile.getOsVersion(); StringBuilder osv = new StringBuilder().append(dcVer.getOsVersionMajor()); if (dcVer.hasOsVersionMinor()) { osv.append('.').append(dcVer.getOsVersionMinor()); if (dcVer.hasOsVersionMicro()) { osv.append('.').append(dcVer.getOsVersionMicro()); } } device.setOsv(osv.toString()); } if (dcMobile.hasMobileDeviceType()) { DeviceType type = DeviceTypeMapper.toOpenRtb(dcMobile.getMobileDeviceType()); if (type != null) { device.setDevicetype(type); } } if (dcMobile.hasScreenWidth()) { device.setW(dcMobile.getScreenWidth()); } if (dcMobile.hasScreenHeight()) { device.setH(dcMobile.getScreenHeight()); } if (dcMobile.hasDevicePixelRatioMillis()) { device.setPxratio(dcMobile.getDevicePixelRatioMillis() / 1000.0); } } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbDevice(dcRequest, device); } return device; } protected @Nullable Geo.Builder buildGeo(NetworkBid.BidRequest dcRequest) { if (!dcRequest.hasGeoCriteriaId() && !dcRequest.hasEncryptedHyperlocalSet() && !dcRequest.hasPostalCode() && !dcRequest.hasPostalCodePrefix()) { return null; } Geo.Builder geo = Geo.newBuilder(); if (dcRequest.hasPostalCode()) { geo.setZip(dcRequest.getPostalCode()); } else if (dcRequest.hasPostalCodePrefix()) { geo.setZip(dcRequest.getPostalCodePrefix()); } if (dcRequest.hasGeoCriteriaId()) { int geoCriteriaId = dcRequest.getGeoCriteriaId(); GeoTarget geoTarget = metadata.geoTargetFor(geoCriteriaId); if (geoTarget == null) { invalidGeoId.inc(); if (logger.isDebugEnabled()) { logger.debug("Received unknown geo_criteria_id: {}", geoCriteriaId); } } else { mapGeo(geoTarget, geo); } } NetworkBid.BidRequest.HyperlocalSet hyperlocalSet = null; if (dcRequest.hasEncryptedHyperlocalSet() && hyperlocalCrypto != null) { try { hyperlocalSet = NetworkBid.BidRequest.HyperlocalSet.parseFrom( hyperlocalCrypto.decryptHyperlocal(dcRequest.getEncryptedHyperlocalSet().toByteArray())); if (hyperlocalSet.hasCenterPoint()) { NetworkBid.BidRequest.Hyperlocal.Point center = hyperlocalSet.getCenterPoint(); if (center.hasLatitude() && center.hasLongitude()) { geo.setLat(center.getLatitude()); geo.setLon(center.getLongitude()); } } } catch (InvalidProtocolBufferException | SignatureException | IllegalArgumentException e) { invalidHyperlocal.inc(); logger.warn("Invalid encrypted_hyperlocal_set: {}", e.toString()); } } if (dcRequest.hasTimezoneOffset()) { geo.setUtcoffset(dcRequest.getTimezoneOffset()); } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbGeo(dcRequest, geo, hyperlocalSet); } return geo; } protected void mapGeo(GeoTarget geoTarget, Geo.Builder geo) { for (int chain = 0; chain < 2; ++chain) { String countryAlpha2 = null; int cityCriteriaId = -1; String dmaRegionName = null; for (GeoTarget target = geoTarget; target != null; // Looks up the canonical chain last, so its results overwrite those // obtained by the parentId chain if there's conflict target = (chain == 0) ? target.idParent() : target.canonParent()) { if (target.countryCode() != null) { countryAlpha2 = target.countryCode(); } switch (target.type()) { case CITY: case PREFECTURE: geo.setCity(target.name()); geo.setCountry(target.countryCode()); cityCriteriaId = target.criteriaId(); break; case DMA_REGION: dmaRegionName = target.name(); break; case STATE: case REGION: geo.setRegion(target.name()); break; default: } } if (countryAlpha2 != null) { CountryCodes countryCodes = metadata.countryCodes().get(countryAlpha2); if (countryCodes != null) { geo.setCountry(countryCodes.alpha3()); } } if (cityCriteriaId != -1 && dmaRegionName != null) { CityDMARegionValue dma = metadata.dmaRegions() .get(new CityDMARegionKey(cityCriteriaId, dmaRegionName)); if (dma != null) { geo.setMetro(String.valueOf(dma.regionCode())); } } } } protected @Nullable App.Builder buildApp(NetworkBid.BidRequest dcRequest) { NetworkBid.BidRequest.Mobile dcMobile = dcRequest.getMobile(); App.Builder app = App.newBuilder(); boolean mapped = false; Content.Builder content = buildAppContent(dcRequest); if (content != null) { app.setContent(content); mapped = true; } if (dcMobile.hasAppId()) { app.setBundle(dcMobile.getAppId()); mapped = true; } if (dcMobile.hasAppName()) { app.setName(dcMobile.getAppName()); mapped = true; } String channelId = findChannelId(dcRequest); if (channelId != null) { app.setId(channelId); mapped = true; } Publisher.Builder pub = buildPublisher(dcRequest); if (pub != null) { app.setPublisher(pub); mapped = true; } for (ExtMapper extMapper : extMappers) { mapped |= extMapper.toOpenRtbApp(dcRequest, app); } return mapped ? app : null; } protected @Nullable Content.Builder buildAppContent(NetworkBid.BidRequest dcRequest) { Content.Builder content = buildContent(dcRequest); if (content == null) { if (!dcRequest.hasUrl() && !dcRequest.hasAnonymousId() && !dcRequest.getMobile().hasAppRating()) { return null; } else { content = Content.newBuilder(); } } if (dcRequest.hasUrl()) { content.setUrl(dcRequest.getUrl()); } else if (dcRequest.hasAnonymousId()) { content.setId(dcRequest.getAnonymousId()); } if (dcRequest.getMobile().hasAppRating()) { content.setUserrating(String.valueOf(dcRequest.getMobile().getAppRating())); } return content; } protected @Nullable Site.Builder buildSite(NetworkBid.BidRequest dcRequest) { Site.Builder site = Site.newBuilder(); boolean mapped = false; Builder content = buildContent(dcRequest); if (content != null) { site.setContent(content); mapped = true; } if (dcRequest.hasUrl()) { site.setPage(dcRequest.getUrl()); mapped = true; } else if (dcRequest.hasAnonymousId()) { site.setName(dcRequest.getAnonymousId()); mapped = true; } Publisher.Builder pub = buildPublisher(dcRequest); if (pub != null) { site.setPublisher(pub); mapped = true; } if (dcRequest.getMobile().hasIsMobileWebOptimized()) { site.setMobile(dcRequest.getMobile().getIsMobileWebOptimized()); mapped = true; } String channelId = findChannelId(dcRequest); if (channelId != null) { site.setId(channelId); mapped = true; } for (ExtMapper extMapper : extMappers) { mapped |= extMapper.toOpenRtbSite(dcRequest, site); } return mapped ? site : null; } protected @Nullable Content.Builder buildContent(NetworkBid.BidRequest dcRequest) { if (dcRequest.getDetectedLanguageCount() == 0 && dcRequest.getDetectedContentLabelCount() == 0 && !dcRequest.getVideo().hasContentAttributes()) { return null; } Content.Builder content = Content.newBuilder(); int nLangs = dcRequest.getDetectedLanguageCount(); if (nLangs == 1) { content.setLanguage(getLanguage(dcRequest.getDetectedLanguage(0))); } else if (nLangs != 0) { StringBuilder sb = new StringBuilder(nLangs * 3 - 1); for (String langCulture : dcRequest.getDetectedLanguageList()) { if (sb.length() != 0) { sb.append(','); } sb.append(getLanguage(langCulture)); } content.setLanguage(sb.toString()); } String rating = ContentRatingMapper.toOpenRtb(dcRequest.getDetectedContentLabelList()); if (rating != null) { content.setContentrating(rating); } if (dcRequest.getVideo().hasContentAttributes()) { NetworkBid.BidRequest.Video.ContentAttributes dcContent = dcRequest.getVideo().getContentAttributes(); if (dcContent.hasTitle()) { content.setTitle(dcContent.getTitle()); } if (dcContent.hasDurationSeconds()) { content.setLen(dcContent.getDurationSeconds()); } content.setKeywords(csvJoiner.join(dcContent.getKeywordsList())); } return content; } protected @Nullable Publisher.Builder buildPublisher(NetworkBid.BidRequest dcRequest) { if (!dcRequest.hasSellerNetworkId()) { return null; } Publisher.Builder publisher = Publisher.newBuilder().setId(String.valueOf(dcRequest.getSellerNetworkId())); String sellerNetwork = metadata.sellerNetworks().get(dcRequest.getSellerNetworkId()); if (sellerNetwork != null) { publisher.setName(sellerNetwork); } return publisher; } protected @Nullable Imp.Builder buildImp(NetworkBid.BidRequest dcRequest, NetworkBid.BidRequest.AdSlot dcSlot) { Imp.Builder imp = Imp.newBuilder().setId(String.valueOf(dcSlot.getId())); Long bidFloor = null; for (NetworkBid.BidRequest.AdSlot.MatchingAdData dcAdData : dcSlot.getMatchingAdDataList()) { if (dcAdData.hasMinimumCpmMicros()) { bidFloor = (bidFloor == null) ? dcAdData.getMinimumCpmMicros() : min(bidFloor, dcAdData.getMinimumCpmMicros()); } Pmp.Builder pmp = buildPmp(dcAdData); if (pmp != null) { imp.setPmp(pmp); } } if (bidFloor != null) { imp.setBidfloor(((double) bidFloor) / MICROS_PER_CURRENCY_UNIT); } if (dcRequest.getMobile().hasIsInterstitialRequest()) { imp.setInstl(dcRequest.getMobile().getIsInterstitialRequest()); } if (dcSlot.hasAdBlockKey()) { imp.setTagid(String.valueOf(dcSlot.getAdBlockKey())); } if (dcSlot.getNativeAdTemplateCount() != 0) { Native.Builder nativ = nativeMapper.buildNativeRequest(dcSlot); if (nativ != null) { imp.setNative(nativ); } } else if (dcRequest.hasVideo()) { Video.Builder video = buildVideo(dcSlot, dcRequest.getVideo()); if (video != null) { imp.setVideo(video); } } else { Banner.Builder banner = buildBanner(dcSlot); if (banner != null) { imp.setBanner(banner); } } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbImp(dcSlot, imp); } return imp.hasVideo() || imp.hasBanner() || imp.hasNative() ? imp : null; } protected @Nullable Pmp.Builder buildPmp(NetworkBid.BidRequest.AdSlot.MatchingAdData dcAdData) { if (dcAdData.getDirectDealCount() == 0) { return null; } Pmp.Builder pmp = Pmp.newBuilder(); for (NetworkBid.BidRequest.AdSlot.MatchingAdData.DirectDeal dcDeal : dcAdData.getDirectDealList()) { Deal.Builder deal = buildDeal(dcDeal); if (deal != null) { pmp.addDeals(deal); } } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbPmp(dcAdData, pmp); } return pmp; } protected @Nullable Deal.Builder buildDeal(NetworkBid.BidRequest.AdSlot.MatchingAdData.DirectDeal dcDeal) { if (!dcDeal.hasDirectDealId()) { return null; } Deal.Builder deal = Deal.newBuilder().setId(String.valueOf(dcDeal.getDirectDealId())); if (dcDeal.hasFixedCpmMicros()) { deal.setBidfloor(dcDeal.getFixedCpmMicros() / ((double) MICROS_PER_CURRENCY_UNIT)); } if (dcDeal.hasDealType()) { AuctionType type = DealTypeMapper.toOpenRtb(dcDeal.getDealType()); if (type != null) { deal.setAt(type); } } return deal; } protected void setSize(Banner.Builder banner, List<Integer> widths, List<Integer> heights) { if (!widths.isEmpty()) { int wMin = Integer.MAX_VALUE, wMax = 0, hMin = Integer.MAX_VALUE, hMax = 0; for (int sizeIndex = 0; sizeIndex < widths.size(); ++sizeIndex) { int w = widths.get(sizeIndex); wMin = min(wMin, w); wMax = max(wMax, w); int h = heights.get(sizeIndex); hMin = min(hMin, h); hMax = max(hMax, h); } if (wMin == wMax) { banner.setW(wMin); } else { banner.setWmin(wMin); banner.setWmax(wMax); } if (hMin == hMax) { banner.setH(hMin); } else { banner.setHmin(hMin); banner.setHmax(hMax); } } } protected Video.Builder buildVideo(NetworkBid.BidRequest.AdSlot dcSlot, NetworkBid.BidRequest.Video dcVideo) { Video.Builder video = Video.newBuilder().addProtocols(VideoBidResponseProtocol.VAST_2_0) .addProtocols(VideoBidResponseProtocol.VAST_3_0) .addAllBattr(CreativeAttributeMapper.toOpenRtb(dcSlot.getExcludedAttributeList(), null)); if (dcVideo.hasMinAdDuration()) { video.setMinduration(dcVideo.getMinAdDuration()); } if (dcVideo.hasMaxAdDuration()) { video.setMaxduration(dcVideo.getMaxAdDuration()); } if (dcVideo.getAllowedVideoFormatsCount() != 0) { video.addAllMimes(VideoMimeMapper.toOpenRtb(dcVideo.getAllowedVideoFormatsList(), null)); } if (dcSlot.hasSlotVisibility()) { AdPosition pos = AdPositionMapper.toOpenRtb(dcSlot.getSlotVisibility()); if (pos != null) { video.setPos(pos); } } if (dcVideo.hasVideoadStartDelay()) { video.setStartdelay(VideoStartDelayMapper.toDoubleClick(dcVideo.getVideoadStartDelay())); } if (!dcSlot.getExcludedAttributeList().contains(30 /* InstreamVastVideoType: Vpaid Flash */)) { video.addApi(APIFramework.VPAID_1); video.addApi(APIFramework.VPAID_2); } if (!dcSlot.getExcludedAttributeList().contains(32 /* MraidType: Mraid 1.0 */)) { video.addApi(APIFramework.MRAID_1); } video.addApi(APIFramework.MRAID_2); if (dcSlot.getWidthCount() == 1) { video.setW(dcSlot.getWidth(0)); video.setH(dcSlot.getHeight(0)); } else if (dcSlot.getWidthCount() != 0) { logger.debug("Invalid Video, cannot be multisize"); return null; } if (dcVideo.getCompanionSlotCount() != 0) { EnumSet<VASTCompanionType> companionTypes = EnumSet.noneOf(VASTCompanionType.class); for (NetworkBid.BidRequest.Video.CompanionSlot dcCompSlot : dcVideo.getCompanionSlotList()) { Banner.Builder companion = Banner.newBuilder(); setSize(companion, dcCompSlot.getWidthList(), dcCompSlot.getHeightList()); if (dcCompSlot.getCreativeFormatCount() != 0) { companion.addAllMimes(BannerMimeMapper.toOpenRtb(dcCompSlot.getCreativeFormatList(), null)); CompanionTypeMapper.toOpenRtb(dcCompSlot.getCreativeFormatList(), companionTypes); } video.addCompanionad(companion); } video.addAllCompaniontype(companionTypes); } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbVideo(dcVideo, video); } return video; } protected Banner.Builder buildBanner(NetworkBid.BidRequest.AdSlot dcSlot) { Banner.Builder banner = Banner.newBuilder().setId(String.valueOf(dcSlot.getId())) .addAllBattr(CreativeAttributeMapper.toOpenRtb(dcSlot.getExcludedAttributeList(), null)); setSize(banner, dcSlot.getWidthList(), dcSlot.getHeightList()); if (dcSlot.hasSlotVisibility()) { AdPosition pos = AdPositionMapper.toOpenRtb(dcSlot.getSlotVisibility()); if (pos != null) { banner.setPos(pos); } } if (!dcSlot.getExcludedAttributeList().contains(32 /* MraidType: Mraid 1.0 */)) { banner.addApi(APIFramework.MRAID_1); } banner.addApi(APIFramework.MRAID_2); banner.addAllExpdir(ExpandableDirectionMapper.toOpenRtb(dcSlot.getExcludedAttributeList())); if (dcSlot.hasIframingState()) { Boolean f = IFramingStateMapper.toOpenRtb(dcSlot.getIframingState()); if (f != null) { banner.setTopframe(f); } } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbBanner(dcSlot, banner); } return banner; } protected @Nullable User.Builder buildUser(NetworkBid.BidRequest dcRequest, boolean coppa) { if ((!coppa && !dcRequest.hasGoogleUserId()) || (coppa && !dcRequest.hasConstrainedUsageGoogleUserId())) { return null; } User.Builder user = User.newBuilder() .setId(coppa ? dcRequest.getConstrainedUsageGoogleUserId() : dcRequest.getGoogleUserId()); if ((coppa && dcRequest.hasConstrainedUsageHostedMatchData()) || (!coppa && dcRequest.hasHostedMatchData())) { ByteString dcHMD = coppa ? dcRequest.getConstrainedUsageHostedMatchData() : dcRequest.getHostedMatchData(); user.setCustomdata(BaseEncoding.base64Url().omitPadding().encode(dcHMD.toByteArray())); } if (dcRequest.hasUserDemographic()) { NetworkBid.BidRequest.UserDemographic dcUser = dcRequest.getUserDemographic(); if (dcUser.hasGender()) { User.Gender gender = GenderMapper.toOpenRtb(dcUser.getGender()); if (gender != null) { user.setGender(OpenRtbUtils.genderToJsonName(gender)); } } if (dcUser.hasAgeLow() || dcUser.hasAgeHigh()) { // OpenRTB only supports a single age, not a range. We have to be pessimistic; // if the age range is [X...Y], assume X to be the age (the youngest possible). // We don't want to get in trouble e.g. if the age is [14..30], using the high // or even average would classify as adult an user that's possibly minor. // If the publisher is known to slot users into certain standard ranges, you // can translate this back, i.e. age 25 could mean the [25-34] range. int age = dcUser.hasAgeHigh() ? dcUser.getAgeHigh() : dcUser.getAgeLow(); Calendar today = Calendar.getInstance(); user.setYob(today.get(Calendar.YEAR) - age); } } if (dcRequest.getDetectedVerticalCount() != 0) { Data.Builder data = OpenRtb.BidRequest.Data.newBuilder().setId("DetectedVerticals") .setName("DoubleClick"); for (NetworkBid.BidRequest.Vertical dcVertical : dcRequest.getDetectedVerticalList()) { Segment.Builder segment = Segment.newBuilder().setId(String.valueOf(dcVertical.getId())) .setValue(String.valueOf(dcVertical.getWeight())); String name = metadata.publisherVerticals().get(dcVertical.getId()); if (name != null) { segment.setName(name); } data.addSegment(segment); } user.addData(data); } for (ExtMapper extMapper : extMappers) { extMapper.toOpenRtbUser(dcRequest, user); } return user; } protected @Nullable String findChannelId(NetworkBid.BidRequest dcRequest) { for (NetworkBid.BidRequest.AdSlot dcSlot : dcRequest.getAdslotList()) { for (String dcChannel : dcSlot.getTargetableChannelList()) { if (dcChannel.startsWith(YOUTUBE_AFV_USER_ID)) { return dcChannel.substring(YOUTUBE_AFV_USER_ID.length()); } else if (SEMITRANSPARENT_CHANNEL.matcher(dcChannel).matches()) { return dcChannel; } } } return null; } @Override public NetworkBid.BidResponse.Builder toExchangeBidResponse(OpenRtb.BidRequest request, OpenRtb.BidResponse response) { checkNotNull(request); NetworkBid.BidResponse.Builder dcResponse = NetworkBid.BidResponse.newBuilder(); if (response.hasBidid()) { dcResponse.setDebugString(response.getBidid()); } for (SeatBid seatBid : response.getSeatbidList()) { for (Bid bid : seatBid.getBidList()) { dcResponse.addAd(buildResponseAd(request, bid)); } } return dcResponse; } protected NetworkBid.BidResponse.Ad.Builder buildResponseAd(OpenRtb.BidRequest request, Bid bid) { NetworkBid.BidResponse.Ad.Builder dcAd; if (bid.hasExtension(DcExt.ad)) { dcAd = bid.getExtension(DcExt.ad).toBuilder(); } else { dcAd = NetworkBid.BidResponse.Ad.newBuilder(); } if (!bid.hasCrid()) { missingCrid.inc(); throw new MapperException("Bid.crid is not set, mandatory for DoubleClick"); } dcAd.setBuyerCreativeId(bid.getCrid()); Imp matchingImp = OpenRtbUtils.impWithId(request, bid.getImpid()); if (matchingImp == null) { invalidImp.inc(); throw new MapperException("Impresson.id doesn't match any request impression: %s", bid.getImpid()); } if (matchingImp.hasVideo()) { dcAd.setVideoUrl(bid.getAdm()); setAdSize(bid, dcAd, matchingImp); } else if (matchingImp.hasBanner()) { if (dcAd.getTemplateParameterCount() == 0) { dcAd.setHtmlSnippet(bid.getAdm()); } else { if (!dcAd.hasSnippetTemplate()) { if (bid.hasAdm()) { logger.debug("Ad fragment has snippetTemplate, ignoring bid's adm"); } dcAd.setSnippetTemplate(bid.getAdm()); } } setAdSize(bid, dcAd, matchingImp); } else if (matchingImp.hasNative()) { dcAd.setNativeAd(nativeMapper.buildNativeResponse(bid, matchingImp)); } else { noVideoOrBanner.inc(); throw new MapperException("Imp has neither of Video or Banner"); } NetworkBid.BidResponse.Ad.AdSlot.Builder dcSlot = dcAd.addAdslotBuilder() .setId(Integer.parseInt(bid.getImpid())) .setMaxCpmMicros((long) (bid.getPrice() * MICROS_PER_CURRENCY_UNIT)); if (matchingImp.getExtension(DcExt.adSlot).getMatchingAdDataCount() > 1) { if (bid.hasCid()) { dcSlot.setAdgroupId(Long.parseLong(bid.getCid())); } else { noCid.inc(); if (logger.isDebugEnabled()) { logger.debug("Missing cid in a Bid created for multi-campaign Imp: {}", TextFormat.shortDebugString(bid)); } } } if (bid.hasDealid()) { dcSlot.setDealId(Long.parseLong(bid.getDealid())); } dcAd.addAllAttribute(CreativeAttributeMapper.toDoubleClick(bid.getAttrList(), null)); if (bid.hasNurl()) { dcAd.setImpressionTrackingUrl(bid.getNurl()); } Set<Integer> cats = new LinkedHashSet<>(); for (String catName : bid.getCatList()) { try { ContentCategory cat = OpenRtbUtils.categoryFromName(catName); cats.addAll(AdCategoryMapper.toDoubleClick(cat)); } catch (IllegalArgumentException e) { invalidContentCategory.inc(); } } dcAd.addAllCategory(cats); for (ExtMapper extMapper : extMappers) { extMapper.toDoubleClickAd(request, bid, dcAd); } return dcAd; } protected void setAdSize(Bid bid, NetworkBid.BidResponse.Ad.Builder dcAd, Imp matchingImp) { boolean multisize = matchingImp.getExtension(DcExt.adSlot).getWidthCount() > 1; if (multisize || matchingImp.getInstl()) { if (bid.hasW() && bid.hasH()) { dcAd.setWidth(bid.getW()); dcAd.setHeight(bid.getH()); } else { missingSize.inc(); if (logger.isDebugEnabled()) { logger.debug("Missing size in a Bid created for {} impression: {}", multisize ? "multisize" : "interstitial", TextFormat.shortDebugString(bid)); } } } } protected static String getLanguage(String langCulture) { int iSep = langCulture.indexOf('_'); return iSep == -1 ? langCulture : langCulture.substring(0, iSep); } /** * Not implemented yet! */ @Override public NetworkBid.BidRequest.Builder toExchangeBidRequest(OpenRtb.BidRequest request) { throw new UnsupportedOperationException(); } /** * Not implemented yet! */ @Override public OpenRtb.BidResponse.Builder toOpenRtbBidResponse(NetworkBid.BidRequest request, NetworkBid.BidResponse response) { throw new UnsupportedOperationException(); } }