How do I make a HTTP PUT call from XLRelease to update data in Adobe Workfront? - rest

I am attempting to make an HTTP PUT request from XLRelease to update data in Adobe Workfront. I have been able to successfully login using the API client and GET data. I have also been able to successfully update data using Postman as well as using a native Python script. I am using the HttpRequest library within XLR. I am receiving the same response back in XLR as I am when successfully updating when using Postman, however, the data is not updated when using XLR.
My code is as follows:
import json
WORKFRONT_API_HOST = releaseVariables['url']
WORKFRONT_API_VERSION = releaseVariables['wfApiVersion']
WORKFRONT_API_KEY = releaseVariables['apiKey']
WORKFRONT_USERNAME = releaseVariables['wfUsername']
FI_ID = releaseVariables['target_customer_id']
newPortfolioId = releaseVariables['target_portfolio_id']
WORKFRONT_API_URL = WORKFRONT_API_HOST + WORKFRONT_API_VERSION
def wfLogin():
sessionID = ""
login_endpoint = "/login"
login_request = HttpRequest({'url': WORKFRONT_API_URL})
login_response = login_request.get(login_endpoint + "?apiKey=" + str(WORKFRONT_API_KEY).replace("u'","'") + "&username=" + WORKFRONT_USERNAME, contentType='application/json')
if login_response.getStatus() != 200:
print('# Error logging into WF\n')
print(login_response.getStatus())
print(login_response.errorDump())
sys.exit(1)
else:
json_response = json.loads(login_response.getResponse())
print ("Logged in to WF")
sessionID = json_response['data']['sessionID']
return sessionID
def wfLogout(sessionID):
logout_endpoint = "/logout"
logout_request = HttpRequest({'url': WORKFRONT_API_URL})
logout_response = logout_request.get(logout_endpoint + "?sessionID=" + sessionID, contentType='application/json')
if logout_response.getStatus() != 200:
print('# Error logging out of WF\n')
print(logout_response.getStatus())
print(logout_response.errorDump())
sys.exit(1)
else:
json_response = json.loads(logout_response.getResponse())
print ("Logged out of WF")
result = []
session_id = wfLogin()
if session_id != "":
customer_request = HttpRequest({'url': WORKFRONT_API_URL})
endpoint = '/prgm/%s?sessionID=%s&portfolioID=%s&customerID=%s' % (FI_ID, session_id, newPortfolioId, FI_ID)
jsonObj = "{}"
payload = {}
customer_response = customer_request.put(endpoint, jsonObj, contentType='application/json')
if customer_response.getStatus() != 200:
print('# Error connecting to WF\n')
print(customer_response)
print(customer_response.getStatus())
print(customer_response.errorDump())
sys.exit(1)
else:
response_json = json.loads(customer_response.getResponse())
print ("response_json: ", response_json)
#log out of current session
wfLogout(session_id)
else:
print ("No sessionID is available")
sys.exit(1)

Related

FastAPI Pytest why does client returns 422

I'm trying to implement testing my post route. It works in my project. I have problems only with pytest.
main.py:
#app.post('/create_service', status_code=status.HTTP_201_CREATED)
async def post_service(
response: Response, service: CreateService, db: Session = Depends(get_db)
):
service_model = models.Service()
service_name = service.name
service_instance = db.query(models.Service).filter(
models.Service.name == service_name
).first()
if service_instance is None:
service_model.name = service_name
db.add(service_model)
db.commit()
serviceversion_model = models.ServiceVersion()
service_instance = db.query(models.Service).filter(
models.Service.name == service_name
).first()
serviceversion_instance = db.query(models.ServiceVersion).filter(
models.ServiceVersion.service_id == service_instance.id
).filter(models.ServiceVersion.version == service.version).first()
if serviceversion_instance:
raise HTTPException(
status_code=400, detail='Version of service already exists'
)
serviceversion_model.version = service.version
serviceversion_model.is_used = service.is_used
serviceversion_model.service_id = service_instance.id
db.add(serviceversion_model)
db.commit()
db.refresh(serviceversion_model)
service_dict = service.dict()
for key in list(service_dict):
if isinstance(service_dict[key], list):
sub_dicts = service_dict[key]
if not sub_dicts:
response.status_code = status.HTTP_400_BAD_REQUEST
return HTTPException(status_code=400, detail='No keys in config')
servicekey_models = []
for i in range(len(sub_dicts)):
servicekey_model = models.ServiceKey()
servicekey_models.append(servicekey_model)
servicekey_models[i].service_id = service_instance.id
servicekey_models[i].version_id = serviceversion_model.id
servicekey_models[i].service_key = sub_dicts[i].get('service_key')
servicekey_models[i].service_value = sub_dicts[i].get('service_value')
db.add(servicekey_models[i])
db.commit()
return 'created'
test_main.py:
def test_create_service(client, db: Session = Depends(get_db)):
key = Key(service_key='testkey1', service_value='testvalue1')
service = CreateService(
name="testname1",
version="testversion1",
is_used=True,
keys=[key, ]
)
response = client.post("/create_service", params={"service": service.dict()})
assert response.status_code == 200
I tried to post service as json, as CreateService instance and finally as params dictionary. I have no errors at response line only with the last one . But I got 422 response status code. What is wrong?
If it can help:
schemas.py
class Key(BaseModel):
service_key: str
service_value: str
class CreateService(BaseModel):
name: str
version: str
is_used: bool
keys: list[Key]
class Config:
orm_mode = True
Don't use params. Use json:
response = client.post("/create_service", json=service.dict())

Getting Invalid Response from API . - Groovy Scripting

I am trying a POST method in groovy which basically creates a Issue in Jira . I am getting invalid response from the API . However in postman I could able to get the response . I have searched few questions in stackoverflow where they are recommending to set the Accept-Encoding to "" . I tried out different methods but it didnt work .
Code :
import javax.net.ssl.*;
import java.security.cert.*;
import groovy.json.JsonBuilder;
import groovy.json.JsonOutput;
import groovy.json.JsonSlurperClassic;
import groovy.json.StreamingJsonBuilder;
def jiraurl = "xxxxx";
def netProxyAddr = "xxxx";
def netProxyPort = 80;
try {
// NOTE: Adjust the following as needed
def uri = jiraurl + "/issue";
def jiraTicketMap = [:];
jiraTicketMap["fields"] = [:];
jiraTicketMap["fields"]["project"]= [:];
jiraTicketMap["fields"]["project"]["id"] = "12412"
jiraTicketMap["fields"]["summary"] = "Test ticket for API";
jiraTicketMap["fields"]["assignee"] = [:];
jiraTicketMap["fields"]["assignee"]["name"] = "*********";
jiraTicketMap["fields"]["customfield_10700"] = [:];
jiraTicketMap["fields"]["customfield_10700"]["value"] = "Normal";
jiraTicketMap["fields"]["customfield_16901"] = "nothing impacted";
jiraTicketMap["fields"]["customfield_16900"] = "testing ";
jiraTicketMap["fields"]["customfield_10710"] = "removing the URL" ;
jiraTicketMap["fields"]["customfield_10711"] = "********";
jiraTicketMap["fields"]["customfield_16902"] = "descriptiong";
jiraTicketMap["fields"]["customfield_10702"] = [:];
jiraTicketMap["fields"]["customfield_10702"]["value"] ="Low";
jiraTicketMap["fields"]["customfield_10900"] = [:];
jiraTicketMap["fields"]["customfield_10900"]["value"] = "Yes";
jiraTicketMap["fields"]["customfield_10703"] = [:];
jiraTicketMap["fields"]["customfield_10703"]["value"] = "Low";
jiraTicketMap["fields"]["customfield_10705"] = "2022-03-19T10:29:29.908+1100";
jiraTicketMap["fields"]["customfield_10706"] = "2022-03-20T10:29:29.908+1100";
jiraTicketMap["fields"]["customfield_10707"] = "testing done";
jiraTicketMap["fields"]["customfield_10708"] = "Implemtation test done for zscaler";
jiraTicketMap["fields"]["customfield_10709"] = "post implention done";
jiraTicketMap["fields"]["issuetype"] = [:];
jiraTicketMap["fields"]["issuetype"]["id"]= "10200";
jiraTicketMap["fields"]["labels"] = ["test"];
jiraTicketMap["fields"]["customfield_21400"] = "Test Environment is required.";
jiraTicketMap["fields"]["customfield_21500"] = "Back-Out Test Details is required";
DEBUG.println("DEBUG: jiraTicketMap is: ${jiraTicketMap}");
def payloadData = StringUtils.mapToJson(jiraTicketMap);
DEBUG.println("DEBUG: Payload is: ${payloadData}");
DEBUG.println("DEBUG: PayloadData is: ${payloadData}");
DEBUG.println("DEBUG: PayloadData type is:"+payloadData.getClass());
netProxyPort = netProxyPort.toInteger();
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(netProxyAddr, netProxyPort));
def authString = "*********".getBytes().encodeBase64().toString()
def url = new URL(uri);
// NOTE: The following removes all checks against SSL certificates
disableSSL();
def conn = url.openConnection(proxy);
conn.doOutput = true;
// Build the Credentials and HTTP headers
conn.setRequestProperty("Authorization", "Basic ***********");
conn.setRequestProperty("Content-type", "application/JSON;charset=UTF-8");
conn.setRequestProperty("Accept","*/*");
conn.setRequestMethod("POST");
conn.setDoOutput(true);
//BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream(), "UTF8"));
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(),"UTF-8")
writer?.write(payloadData);
writer?.flush();
writer?.close();
if(conn.getResponseCode() == 201 || conn.getResponseCode() == 200 ) {
InputStream responseStream = conn.getInputStream();
result = responseStream.getText("UTF-8");
DEBUG.println("DEBUG:>>>>>>>>>>>>> SUccessfully Posted Data to Jira");
// NOTE: If you need to get the response header, use below
//INPUTS.HEADERS = conn.getHeaderFields();
responseStream.close();
}
else {
INPUTS.RSEXCEPTION = "Response code was " + conn.getResponseCode();
// Attempt to parse error stream.
InputStream responseStream = conn.getErrorStream();
result = responseStream?.getText("UTF-8");
responseStream?.close();
}
}
catch (Exception e) {
//INPUTS.RSEXCEPTION = e.getMessage();
result = e.getMessage();
result += "\n" + e.getClass() + ": " + e.getMessage();
for (trace in e.getStackTrace())
{
result += "\n\t" + trace;
}
}
return result;
Response :
Response from Postman and local code editor
{
"id": "12212",
"key": "WCM-2211",
"self": "https://********/rest/api/2/issue/2496228"
}
Response from Vendor Tool:
�4̱#0��w��^BV��\z(M��J"��u1���?`4tPȶ.���S���7��0�%���q7A M�X����xv…qD�h�O��~��NCe
Response Headers
[X-AREQUESTID:[533x1208409x1], null:[HTTP/1.1 201 Created], Server:[Web Server], X-Content-Type-Options:[nosniff], Connection:[close], X-ANODEID:[node1], Date:[Thu, 12 May 2022 07:53:25 GMT], X-Frame-Options:[SAMEORIGIN], Referrer-Policy:[no-referrer-when-downgrade], Strict-Transport-Security:[max-age=15768000], Cache-Control:[no-cache, no-store, no-transform], Content-Security-Policy:[frame-ancestors 'self'], X-AUSERNAME:[SVC], Content-Encoding:[gzip], X-ASESSIONID:[1l2vszx], Set-Cookie:[akaalb_jira_dev_alb=~op=jira_dev_lb:jira-dev-ld6|~rv=95~m=jia-dev-ld6:0|~os=287df636055edb4e278283~id=de0d2567c76bf80374af7c583f; path=/; HttpOnly; Secure; SameSite=None, JESSIONID=2920944044.37151.0000; path=/; Httponly; Secure, atlassian.xsrf.token=BZXB-Z179-LJQD-6WAV_9b298f0387a7c68a8652e963b69e4_lin; Path=/; Secure, JSESSIONID=27C93900AA8B780AA44C2861F73; Path=/; Secure; HttpOnly], Feature-Policy:[midi 'none'], Vary:[Accept-Encoding, User-Agent], X-XSS-Protection:[1; mode=block], X-Seraph-LoginReason:[OK], X-Powered-By:[Web Server], Content-Type:[application/json;charset=UTF-8]]
Solution from #dagget :
// use the below code when reading it from input stream
accept-encoding : identity
responseStream = new java.util.zip.GZIPInputStream(responseStream)

Mail not getting sent through AWS Glue Python job

I am trying to send a mail through an AWS Glue job. The mail will have multiple number of attachments that it gets from the s3 bucket. According to the logs, it is running until server.login(). It is failing in the server.sendmail() function.
Following is the code -
def sendEmail(TO, SUBJECT, BODY_HTML):
msg = MIMEMultipart('alternative')
msg['Subject'] = SUBJECT
msg['From'] = SENDER
msg['To'] = ','.join(RECIPIENT + TO)
part1 = MIMEText(BODY_HTML, 'html')
msg.attach(part1)
s3 = boto3.resource('s3')
bucket = s3.Bucket('sample-bucket')
for obj in bucket.objects.filter(Delimiter='/', Prefix='sample-folder/'):
filename = ((obj.key).split("/")[1])
s3_object = s3_obj.s3_get_object(sample-bucket, 'sample-folder/'+ filename)
body = s3_object['Body'].read()
part = MIMEApplication(body, filename)
part.add_header("Content-Disposition", 'attachment', filename=filename)
msg.attach(part)
try:
server = smtplib.SMTP(HOST, PORT)
server.ehlo()
server.starttls()
server.ehlo()
server.login(USERNAME_SMTP, PASSWORD_SMTP)
server.sendmail(SENDER, RECIPIENT, msg.as_string()) ***--Error***
server.close()
print (msg)
print ("Email sent")
I am getting the following error -
Error: (554, b'Transaction failed: Expected '=', got "null"')
What is the issue?
I got the answer. The problem was with the way files were read from s3. The output of the first iteration was -
sample-bucket/sample-folder/
So, it was taking a null object and failing. So, I just skipped the first object in the iteration and carried out the whole thing. It worked.
Please find the final code below -
def sendEmail(TO, SUBJECT, BODY_HTML):
msg = MIMEMultipart('alternative')
msg['Subject'] = SUBJECT
msg['From'] = SENDER
msg['To'] = ','.join(RECIPIENT + TO)
part1 = MIMEText(BODY_HTML, 'html')
msg.attach(part1)
s3 = boto3.resource('s3')
bucket = s3.Bucket('sample-bucket')
**it = iter(bucket.objects.filter(Delimiter='/', Prefix='sample-folder/'))
next(it, None)
for obj in it:**
filename = ((obj.key).split("/")[1])
s3_object = s3_obj.s3_get_object(sample-bucket, 'sample-folder/'+ filename)
body = s3_object['Body'].read()
part = MIMEApplication(body, filename)
part.add_header("Content-Disposition", 'attachment', filename=filename)
msg.attach(part)
try:
server = smtplib.SMTP(HOST, PORT)
server.ehlo()
server.starttls()
server.ehlo()
server.login(USERNAME_SMTP, PASSWORD_SMTP)
server.sendmail(SENDER, RECIPIENT, msg.as_string())
server.close()
print (msg)
print ("Email sent")

force.com callout exception Unable to tunnel through proxy

We make a callout from one Salesforce org to another Salesforce org using the REST API. That worked until end of november. We didn't make any changes at the affected classes or configuration.
Now, while sending a request to the rest api a callout exception will be thrown with the message : "Unable to tunnel through proxy. Proxy returns "HTTP/1.0 503 Service Unavailable".
The authorisation to the rest api is done by session id.
Does anyone have any idea what the problem is?
Here the code snipped:
final String WS_ENDPOINT = 'https://login.salesforce.com/services/Soap/c/24.0';
final String REST_ENDPOINT = 'https://eu2.salesforce.com/services/apexrest/UsageReporterService';
final String USERNAME = '*****';
final String PASSWORD = '*****';
HTTP h = new HTTP();
HTTPRequest req = new HTTPRequest();
req.setMethod('POST');
req.setEndpoint(REST_ENDPOINT);
req.setHeader('Content-Type', 'application/json');
req.setTimeout(60000);
HTTP hLogin = new HTTP();
HTTPRequest reqLogin = new HTTPRequest();
reqLogin.setMethod('POST');
reqLogin.setEndpoint(WS_ENDPOINT);
reqLogin.setHeader('Content-Type', 'text/xml');
reqLogin.setHeader('SOAPAction', 'login');
reqLogin.setTimeout(60000);
reqLogin.setCompressed(false);
// get a valid session id
String sessionId;
String loginSoap = '<?xml version="1.0" encoding="UTF-8"?>';
loginSoap += '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:enterprise.soap.sforce.com">';
loginSoap += '<soapenv:Body>';
loginSoap += '<urn:login>';
loginSoap += '<urn:username>' + USERNAME + '</urn:username>';
loginSoap += '<urn:password>' + PASSWORD + '</urn:password>';
loginSoap += '</urn:login>';
loginSoap += '</soapenv:Body>';
loginSoap += '</soapenv:Envelope>';
reqLogin.setBody(loginSoap);
HTTPResponse respLogin;
try {
respLogin = hLogin.send(reqLogin);
} catch(CalloutException c){
return null;
}
System.debug('++++++'+respLogin.getStatus() + ': ' + respLogin.getBody());
Dom.Document doc = new Dom.Document();
doc.load(respLogin.getBody());
Dom.XMLNode root = doc.getRootElement();
String ns = root.getNamespace();
Dom.XMLNode bodyEl = root.getChildElements()[0];
if(bodyEl.getChildElements()[0].getName().equals('loginResponse')){
sessionId = bodyEl.getChildElements()[0].getChildElement('result', ns).getChildElement('sessionId', ns).getText();
}
// finished getting session Id
if(sessionId != null){ // login was successfull
req.setHeader('Authorization', 'Bearer ' + sessionId);
// serialize data into json string
UsageReporterModel usageReporterData = new UsageReporterModel();
String inputStr = usageReporterData.serialize();
req.setBody('{ "usageReportData" : ' + inputStr + '}');
// fire!
HTTPResponse resp;
try {
resp = h.send(req);
} catch(CalloutException c){
return null;
}
}
I suspect this will relate to a change of IP addresses for one of the org's which haven't been whitelisted correctly (or added to the "network access" object). With it being Salesforce to Salesforce I would hope that Salesforce.com support can assist?

google maps, cellid to location

According to this sample:
http://www.codeproject.com/KB/mobile/DeepCast.aspx
It's possible to request a gps coordinate (longitude & latitude) including range when sending cellid information (MCC, MNC, towerid, etc)
Can someone tell me the actual parameter to request/post to this address?
http://www.google.com/glm/mmap
It could be something like this
http://www.google.com/glm/mmap?mcc=xxx&mnc=xxx&towerid=xxx
And i would like to know what response we would get.
I have observe OpenCellid website and they provide some nice API to begin with, but i want to know about that in google map too (since they have more completed database).
OpenCellID API
Here is example for work with
#!/usr/bin/python
country = 'fr'
#device = 'Sony_Ericsson-K750'
device = "Nokia N95 8Gb"
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
mmap_url = 'http://www.google.com/glm/mmap'
geo_url = 'http://maps.google.com/maps/geo'
from struct import pack, unpack
from httplib import HTTP
import urllib2
def fetch_latlong_http(query):
http = HTTP('www.google.com', 80)
http.putrequest('POST', '/glm/mmap')
http.putheader('Content-Type', 'application/binary')
http.putheader('Content-Length', str(len(query)))
http.endheaders()
http.send(query)
code, msg, headers = http.getreply()
result = http.file.read()
return result
def fetch_latlong_urllib(query):
headers = { 'User-Agent' : user_agent }
req = urllib2.Request(mmap_url, query, headers)
resp = urllib2.urlopen(req)
response = resp.read()
return response
fetch_latlong = fetch_latlong_http
def get_location_by_cell(cid, lac, mnc=0, mcc=0, country='fr'):
b_string = pack('>hqh2sh13sh5sh3sBiiihiiiiii',
21, 0,
len(country), country,
len(device), device,
len('1.3.1'), "1.3.1",
len('Web'), "Web",
27, 0, 0,
3, 0, cid, lac,
0, 0, 0, 0)
bytes = fetch_latlong(b_string)
(a, b,errorCode, latitude, longitude, c, d, e) = unpack(">hBiiiiih",bytes)
latitude = latitude / 1000000.0
longitude = longitude / 1000000.0
return latitude, longitude
def get_location_by_geo(latitude, longitude):
url = '%s?q=%s,%s&output=json&oe=utf8' % (geo_url, str(latitude), str(longitude))
return urllib2.urlopen(url).read()
if __name__ == '__main__':
print get_location_by_cell(20465, 495, 3, 262)
print get_location_by_cell(20442, 6015)
print get_location_by_cell(1085, 24040)
print get_location_by_geo(40.714224, -73.961452)
print get_location_by_geo(13.749113, 100.565327)
You could use the Google Location API which is used by Firefox (Example see at http://www.mozilla.com/en-US/firefox/geolocation/ ) which has the url www.google.com/loc/json/. In fact this is JSON based webservice and a minimal Perl Example Look like this:
use LWP;
my $ua = LWP::UserAgent->new;
$ua->agent("TestApp/0.1 ");
$ua->env_proxy();
my $req = HTTP::Request->new(POST => 'https://www.google.com/loc/json');
$req->content_type('application/jsonrequest');
$req->content('{"cell_towers": [{"location_area_code": "8721", "mobile_network_code": "01", "cell_id": "7703", "mobile_country_code": "262"}], "version": "1.1.0", "request_address": "true"}');
# Pass request to the user agent and get a response back
my $res = $ua->request($req);
# Check the outcome of the response
if ($res->is_success) {
print $res->content;
} else {
print $res->status_line, "\n";
return undef;
}
Please keep in mind that Google has not officially opened this API for other uses...
The new place for the Google location API is the following :
https://developers.google.com/maps/documentation/geolocation/intro
With this API, you can retrieve a location from Cell information (cellid, mcc, mnc, and lac)
Base on GeolocationAPI, here are some parts of my code:
import java.io.IOException;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
//http://code.google.com/p/google-gson/
import com.google.gson.stream.JsonWriter;
...
/**
* Requests latitude and longitude from Google.
*
* #param gsmParams
* {#link GsmParams}
* #return an {#link HttpURLConnection} containing connection to Google
* lat-long data.
* #throws IOException
*/
public HttpURLConnection requestLatlongFromGoogle(GsmParams gsmParams)
throws IOException {
// prepare parameters for POST method
StringWriter sw = new StringWriter();
JsonWriter jw = new JsonWriter(sw);
try {
jw.beginObject();
jw.name("host").value("localhost");
jw.name("version").value("1.1.0");
jw.name("request_address").value(true);
jw.name("cell_towers");
jw.beginArray().beginObject();
jw.name("cell_id").value(gsmParams.getCid());
jw.name("location_area_code").value(gsmParams.getLac());
jw.name("mobile_network_code").value(gsmParams.getMnc());
jw.name("mobile_country_code").value(gsmParams.getMcc());
jw.endObject().endArray().endObject();
} finally {
try {
jw.close();
} catch (IOException ioe) {
}
try {
sw.close();
} catch (IOException ioe) {
}
}
final String JsonParams = sw.toString();
final String GoogleLocJsonUrl = "http://www.google.com/loc/json";
// post request
URL url = null;
HttpURLConnection conn = null;
url = new URL(GoogleLocJsonUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout((int) 30e3);
conn.setReadTimeout((int) 30e3);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.getOutputStream().write(JsonParams.getBytes());
conn.getOutputStream().flush();
conn.getOutputStream().close();
int resCode = conn.getResponseCode();
if (resCode == Http_BadRequest || resCode != Http_Ok) {
throw new IOException(String.format(
"Response code from Google: %,d", resCode));
}
return conn;
}
The object GsmParams is just a Java bean containing GSM parameters MCC, MNC, LAC, CID. I think you can create a same class easily.
After getting connection, you can call conn.getInputStream() and get results from Google Maps. Then use JsonReader to parse data...
As noted in other threads also check out https://labs.ericsson.com/apis/mobile-location/documentation/cell-id-look-up-api for a free cell-ID database to get coordinates from cellid, mcc, mnc, and lac .