You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2128 lines
81 KiB
Java
2128 lines
81 KiB
Java
/*
|
|
* Copyright 2011 The OpenIEC61850 Authors
|
|
*
|
|
* 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.beanit.openiec61850;
|
|
|
|
import com.beanit.jasn1.ber.ReverseByteArrayOutputStream;
|
|
import com.beanit.jasn1.ber.types.BerBoolean;
|
|
import com.beanit.jasn1.ber.types.BerInteger;
|
|
import com.beanit.jasn1.ber.types.BerNull;
|
|
import com.beanit.jasn1.ber.types.string.BerGraphicString;
|
|
import com.beanit.jasn1.ber.types.string.BerVisibleString;
|
|
import com.beanit.josistack.AcseAssociation;
|
|
import com.beanit.josistack.ByteBufferInputStream;
|
|
import com.beanit.josistack.ClientAcseSap;
|
|
import com.beanit.josistack.DecodingException;
|
|
import com.beanit.openiec61850.internal.mms.asn1.AccessResult;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ConfirmedRequestPDU;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ConfirmedResponsePDU;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ConfirmedServiceRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ConfirmedServiceResponse;
|
|
import com.beanit.openiec61850.internal.mms.asn1.Data;
|
|
import com.beanit.openiec61850.internal.mms.asn1.DefineNamedVariableListRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.DeleteNamedVariableListRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.DeleteNamedVariableListRequest.ListOfVariableListName;
|
|
import com.beanit.openiec61850.internal.mms.asn1.DeleteNamedVariableListResponse;
|
|
import com.beanit.openiec61850.internal.mms.asn1.DirectoryEntry;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileCloseRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileDeleteRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileDirectoryRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileDirectoryResponse;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileName;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileOpenRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.FileReadRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.GetNameListRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.GetNameListRequest.ObjectScope;
|
|
import com.beanit.openiec61850.internal.mms.asn1.GetNameListResponse;
|
|
import com.beanit.openiec61850.internal.mms.asn1.GetNamedVariableListAttributesRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.GetNamedVariableListAttributesResponse;
|
|
import com.beanit.openiec61850.internal.mms.asn1.GetVariableAccessAttributesRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.Identifier;
|
|
import com.beanit.openiec61850.internal.mms.asn1.InitiateRequestPDU;
|
|
import com.beanit.openiec61850.internal.mms.asn1.InitiateResponsePDU;
|
|
import com.beanit.openiec61850.internal.mms.asn1.Integer16;
|
|
import com.beanit.openiec61850.internal.mms.asn1.Integer32;
|
|
import com.beanit.openiec61850.internal.mms.asn1.Integer8;
|
|
import com.beanit.openiec61850.internal.mms.asn1.MMSpdu;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ObjectClass;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ObjectName;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ParameterSupportOptions;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ReadRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ReadResponse;
|
|
import com.beanit.openiec61850.internal.mms.asn1.RejectPDU.RejectReason;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ServiceError.ErrorClass;
|
|
import com.beanit.openiec61850.internal.mms.asn1.ServiceSupportOptions;
|
|
import com.beanit.openiec61850.internal.mms.asn1.UnconfirmedPDU;
|
|
import com.beanit.openiec61850.internal.mms.asn1.UnconfirmedService;
|
|
import com.beanit.openiec61850.internal.mms.asn1.Unsigned32;
|
|
import com.beanit.openiec61850.internal.mms.asn1.VariableAccessSpecification;
|
|
import com.beanit.openiec61850.internal.mms.asn1.VariableDefs;
|
|
import com.beanit.openiec61850.internal.mms.asn1.WriteRequest;
|
|
import com.beanit.openiec61850.internal.mms.asn1.WriteRequest.ListOfData;
|
|
import com.beanit.openiec61850.internal.mms.asn1.WriteResponse;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.IOException;
|
|
import java.net.InetAddress;
|
|
import java.nio.ByteBuffer;
|
|
import java.text.ParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Calendar;
|
|
import java.util.Collection;
|
|
import java.util.Date;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
|
|
/**
|
|
* Represents an association/connection to an IEC 61850 MMS server. An instance of <code>
|
|
* ClientAssociation</code> is obtained using <code>ClientSap</code>. An association object can be
|
|
* used to execute the IEC 61850 ACSI services. Note that not all ACSI services have a corresponding
|
|
* function in this API. For example all GetDirectory and GetDefinition services are covered by
|
|
* <code>retrieveModel()</code>. The control services can be executed by using getDataValues and
|
|
* setDataValues on the control objects in the data model.
|
|
*/
|
|
public final class ClientAssociation {
|
|
|
|
private static final Integer16 version = new Integer16(new byte[] {(byte) 0x01, (byte) 0x01});
|
|
private static final ParameterSupportOptions proposedParameterCbbBitString =
|
|
new ParameterSupportOptions(new byte[] {0x03, 0x05, (byte) 0xf1, 0x00});
|
|
private final ClientReceiver clientReceiver;
|
|
private final BlockingQueue<MMSpdu> incomingResponses = new LinkedBlockingQueue<>();
|
|
private final ReverseByteArrayOutputStream reverseOStream =
|
|
new ReverseByteArrayOutputStream(500, true);
|
|
ServerModel serverModel;
|
|
private AcseAssociation acseAssociation = null;
|
|
private int responseTimeout;
|
|
|
|
private int invokeId = 0;
|
|
|
|
private int negotiatedMaxPduSize;
|
|
private ClientEventListener reportListener = null;
|
|
|
|
private boolean closed = false;
|
|
|
|
ClientAssociation(
|
|
InetAddress address,
|
|
int port,
|
|
InetAddress localAddr,
|
|
int localPort,
|
|
String authenticationParameter,
|
|
ClientAcseSap acseSap,
|
|
int proposedMaxMmsPduSize,
|
|
int proposedMaxServOutstandingCalling,
|
|
int proposedMaxServOutstandingCalled,
|
|
int proposedDataStructureNestingLevel,
|
|
byte[] servicesSupportedCalling,
|
|
int responseTimeout,
|
|
int messageFragmentTimeout,
|
|
ClientEventListener reportListener)
|
|
throws IOException {
|
|
|
|
this.responseTimeout = responseTimeout;
|
|
|
|
acseSap.tSap.setMessageFragmentTimeout(messageFragmentTimeout);
|
|
acseSap.tSap.setMessageTimeout(responseTimeout);
|
|
|
|
negotiatedMaxPduSize = proposedMaxMmsPduSize;
|
|
|
|
this.reportListener = reportListener;
|
|
|
|
associate(
|
|
address,
|
|
port,
|
|
localAddr,
|
|
localPort,
|
|
authenticationParameter,
|
|
acseSap,
|
|
proposedMaxMmsPduSize,
|
|
proposedMaxServOutstandingCalling,
|
|
proposedMaxServOutstandingCalled,
|
|
proposedDataStructureNestingLevel,
|
|
servicesSupportedCalling);
|
|
|
|
acseAssociation.setMessageTimeout(0);
|
|
|
|
clientReceiver = new ClientReceiver(negotiatedMaxPduSize);
|
|
clientReceiver.start();
|
|
}
|
|
|
|
private static ServiceError mmsDataAccessErrorToServiceError(BerInteger dataAccessError) {
|
|
|
|
switch (dataAccessError.value.intValue()) {
|
|
case 1:
|
|
return new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT, "MMS DataAccessError: hardware-fault");
|
|
case 2:
|
|
return new ServiceError(
|
|
ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT,
|
|
"MMS DataAccessError: temporarily-unavailable");
|
|
case 3:
|
|
return new ServiceError(
|
|
ServiceError.ACCESS_VIOLATION, "MMS DataAccessError: object-access-denied");
|
|
case 5:
|
|
return new ServiceError(
|
|
ServiceError.PARAMETER_VALUE_INCONSISTENT, "MMS DataAccessError: invalid-address");
|
|
case 7:
|
|
return new ServiceError(
|
|
ServiceError.TYPE_CONFLICT, "MMS DataAccessError: type-inconsistent");
|
|
case 10:
|
|
return new ServiceError(
|
|
ServiceError.INSTANCE_NOT_AVAILABLE, "MMS DataAccessError: object-non-existent");
|
|
case 11:
|
|
return new ServiceError(
|
|
ServiceError.PARAMETER_VALUE_INCONSISTENT, "MMS DataAccessError: object-value-invalid");
|
|
default:
|
|
return new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"MMS DataAccessError: " + dataAccessError.value);
|
|
}
|
|
}
|
|
|
|
private static void testForErrorResponse(MMSpdu mmsResponsePdu) throws ServiceError {
|
|
if (mmsResponsePdu.getConfirmedErrorPDU() == null) {
|
|
return;
|
|
}
|
|
|
|
ErrorClass errClass = mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getErrorClass();
|
|
|
|
if (errClass != null) {
|
|
if (errClass.getAccess() != null) {
|
|
if (errClass.getAccess().value.intValue() == 3) {
|
|
throw new ServiceError(
|
|
ServiceError.ACCESS_VIOLATION,
|
|
"MMS confirmed error: class: \"access\", error code: \"object-access-denied\"");
|
|
} else if (errClass.getAccess().value.intValue() == 2) {
|
|
|
|
throw new ServiceError(
|
|
ServiceError.INSTANCE_NOT_AVAILABLE,
|
|
"MMS confirmed error: class: \"access\", error code: \"object-non-existent\"");
|
|
}
|
|
|
|
} else if (errClass.getFile() != null) {
|
|
if (errClass.getFile().value.intValue() == 7) {
|
|
|
|
throw new ServiceError(
|
|
ServiceError.FILE_NONE_EXISTENT,
|
|
"MMS confirmed error: class: \"file\", error code: \"file-non-existent\"");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getAdditionalDescription()
|
|
!= null) {
|
|
throw new ServiceError(
|
|
ServiceError.UNKNOWN,
|
|
"MMS confirmed error. Description: "
|
|
+ mmsResponsePdu
|
|
.getConfirmedErrorPDU()
|
|
.getServiceError()
|
|
.getAdditionalDescription()
|
|
.toString());
|
|
}
|
|
throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error.");
|
|
}
|
|
|
|
private static void testForRejectResponse(MMSpdu mmsResponsePdu) throws ServiceError {
|
|
if (mmsResponsePdu.getRejectPDU() == null) {
|
|
return;
|
|
}
|
|
|
|
RejectReason rejectReason = mmsResponsePdu.getRejectPDU().getRejectReason();
|
|
if (rejectReason != null) {
|
|
if (rejectReason.getPduError() != null) {
|
|
if (rejectReason.getPduError().value.intValue() == 1) {
|
|
throw new ServiceError(
|
|
ServiceError.PARAMETER_VALUE_INCONSISTENT,
|
|
"MMS reject: type: \"pdu-error\", reject code: \"invalid-pdu\"");
|
|
}
|
|
}
|
|
}
|
|
throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error.");
|
|
}
|
|
|
|
private static void testForInitiateErrorResponse(MMSpdu mmsResponsePdu) throws ServiceError {
|
|
if (mmsResponsePdu.getInitiateErrorPDU() != null) {
|
|
|
|
ErrorClass errClass = mmsResponsePdu.getInitiateErrorPDU().getErrorClass();
|
|
if (errClass != null) {
|
|
if (errClass.getVmdState() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"vmd_state\" with val: " + errClass.getVmdState().value);
|
|
}
|
|
if (errClass.getApplicationReference() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"application_reference\" with val: "
|
|
+ errClass.getApplicationReference().value);
|
|
}
|
|
if (errClass.getDefinition() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"definition\" with val: " + errClass.getDefinition().value);
|
|
}
|
|
if (errClass.getResource() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"resource\" with val: " + errClass.getResource().value);
|
|
}
|
|
if (errClass.getService() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"service\" with val: " + errClass.getService().value);
|
|
}
|
|
if (errClass.getServicePreempt() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"service_preempt\" with val: " + errClass.getServicePreempt().value);
|
|
}
|
|
if (errClass.getTimeResolution() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"time_resolution\" with val: " + errClass.getTimeResolution().value);
|
|
}
|
|
if (errClass.getAccess() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"access\" with val: " + errClass.getAccess().value);
|
|
}
|
|
if (errClass.getInitiate() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"initiate\" with val: " + errClass.getInitiate().value);
|
|
}
|
|
if (errClass.getConclude() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"conclude\" with val: " + errClass.getConclude());
|
|
}
|
|
if (errClass.getCancel() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"cancel\" with val: " + errClass.getCancel().value);
|
|
}
|
|
if (errClass.getFile() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"file\" with val: " + errClass.getFile().value);
|
|
}
|
|
if (errClass.getOthers() != null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"error class \"others\" with val: " + errClass.getOthers().value);
|
|
}
|
|
}
|
|
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, "unknown error class");
|
|
}
|
|
}
|
|
|
|
private static MMSpdu constructInitRequestPdu(
|
|
int proposedMaxPduSize,
|
|
int proposedMaxServOutstandingCalling,
|
|
int proposedMaxServOutstandingCalled,
|
|
int proposedDataStructureNestingLevel,
|
|
byte[] servicesSupportedCalling) {
|
|
|
|
InitiateRequestPDU.InitRequestDetail initRequestDetail =
|
|
new InitiateRequestPDU.InitRequestDetail();
|
|
initRequestDetail.setProposedVersionNumber(version);
|
|
initRequestDetail.setProposedParameterCBB(proposedParameterCbbBitString);
|
|
initRequestDetail.setServicesSupportedCalling(
|
|
new ServiceSupportOptions(servicesSupportedCalling, 85));
|
|
|
|
InitiateRequestPDU initiateRequestPdu = new InitiateRequestPDU();
|
|
initiateRequestPdu.setLocalDetailCalling(new Integer32(proposedMaxPduSize));
|
|
initiateRequestPdu.setProposedMaxServOutstandingCalling(
|
|
new Integer16(proposedMaxServOutstandingCalling));
|
|
initiateRequestPdu.setProposedMaxServOutstandingCalled(
|
|
new Integer16(proposedMaxServOutstandingCalled));
|
|
initiateRequestPdu.setProposedDataStructureNestingLevel(
|
|
new Integer8(proposedDataStructureNestingLevel));
|
|
initiateRequestPdu.setInitRequestDetail(initRequestDetail);
|
|
|
|
MMSpdu initiateRequestMMSpdu = new MMSpdu();
|
|
initiateRequestMMSpdu.setInitiateRequestPDU(initiateRequestPdu);
|
|
|
|
return initiateRequestMMSpdu;
|
|
}
|
|
|
|
/**
|
|
* Gets the response timeout. The response timeout is used whenever a request is sent to the
|
|
* server. The client will wait for this amount of time for the server's response before throwing
|
|
* a ServiceError.TIMEOUT. Responses received after the timeout will be automatically discarded.
|
|
*
|
|
* @return the response timeout in milliseconds.
|
|
*/
|
|
public int getResponseTimeout() {
|
|
return responseTimeout;
|
|
}
|
|
|
|
/**
|
|
* Sets the response timeout. The response timeout is used whenever a request is sent to the
|
|
* server. The client will wait for this amount of time for the server's response before throwing
|
|
* a ServiceError.TIMEOUT. Responses received after the timeout will be automatically discarded.
|
|
*
|
|
* @param timeout the response timeout in milliseconds.
|
|
*/
|
|
public void setResponseTimeout(int timeout) {
|
|
responseTimeout = timeout;
|
|
}
|
|
|
|
private int getInvokeId() {
|
|
invokeId = (invokeId + 1) % 2147483647;
|
|
return invokeId;
|
|
}
|
|
|
|
private ConfirmedServiceResponse encodeWriteReadDecode(ConfirmedServiceRequest serviceRequest)
|
|
throws ServiceError, IOException {
|
|
|
|
int currentInvokeId = getInvokeId();
|
|
|
|
ConfirmedRequestPDU confirmedRequestPdu = new ConfirmedRequestPDU();
|
|
confirmedRequestPdu.setInvokeID(new Unsigned32(currentInvokeId));
|
|
confirmedRequestPdu.setService(serviceRequest);
|
|
|
|
MMSpdu requestPdu = new MMSpdu();
|
|
requestPdu.setConfirmedRequestPDU(confirmedRequestPdu);
|
|
|
|
reverseOStream.reset();
|
|
|
|
try {
|
|
requestPdu.encode(reverseOStream);
|
|
} catch (Exception e) {
|
|
IOException e2 = new IOException("Error encoding MmsPdu.", e);
|
|
clientReceiver.close(e2);
|
|
throw e2;
|
|
}
|
|
|
|
clientReceiver.setResponseExpected(currentInvokeId);
|
|
try {
|
|
acseAssociation.send(reverseOStream.getByteBuffer());
|
|
} catch (IOException e) {
|
|
IOException e2 = new IOException("Error sending packet.", e);
|
|
clientReceiver.close(e2);
|
|
throw e2;
|
|
}
|
|
|
|
MMSpdu decodedResponsePdu = null;
|
|
|
|
try {
|
|
if (responseTimeout == 0) {
|
|
decodedResponsePdu = incomingResponses.take();
|
|
} else {
|
|
decodedResponsePdu = incomingResponses.poll(responseTimeout, TimeUnit.MILLISECONDS);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
}
|
|
|
|
if (decodedResponsePdu == null) {
|
|
decodedResponsePdu = clientReceiver.removeExpectedResponse();
|
|
if (decodedResponsePdu == null) {
|
|
throw new ServiceError(ServiceError.TIMEOUT);
|
|
}
|
|
}
|
|
|
|
if (decodedResponsePdu.getConfirmedRequestPDU() != null) {
|
|
incomingResponses.add(decodedResponsePdu);
|
|
throw new IOException("connection was closed", clientReceiver.getLastIOException());
|
|
}
|
|
|
|
testForInitiateErrorResponse(decodedResponsePdu);
|
|
testForErrorResponse(decodedResponsePdu);
|
|
testForRejectResponse(decodedResponsePdu);
|
|
|
|
ConfirmedResponsePDU confirmedResponsePdu = decodedResponsePdu.getConfirmedResponsePDU();
|
|
if (confirmedResponsePdu == null) {
|
|
throw new IllegalStateException("Response PDU is not a confirmed response pdu");
|
|
}
|
|
|
|
return confirmedResponsePdu.getService();
|
|
}
|
|
|
|
private void associate(
|
|
InetAddress address,
|
|
int port,
|
|
InetAddress localAddr,
|
|
int localPort,
|
|
String authenticationParameter,
|
|
ClientAcseSap acseSap,
|
|
int proposedMaxPduSize,
|
|
int proposedMaxServOutstandingCalling,
|
|
int proposedMaxServOutstandingCalled,
|
|
int proposedDataStructureNestingLevel,
|
|
byte[] servicesSupportedCalling)
|
|
throws IOException {
|
|
|
|
MMSpdu initiateRequestMMSpdu =
|
|
constructInitRequestPdu(
|
|
proposedMaxPduSize,
|
|
proposedMaxServOutstandingCalling,
|
|
proposedMaxServOutstandingCalled,
|
|
proposedDataStructureNestingLevel,
|
|
servicesSupportedCalling);
|
|
|
|
ReverseByteArrayOutputStream reverseOStream = new ReverseByteArrayOutputStream(500, true);
|
|
initiateRequestMMSpdu.encode(reverseOStream);
|
|
|
|
try {
|
|
acseAssociation =
|
|
acseSap.associate(
|
|
address,
|
|
port,
|
|
localAddr,
|
|
localPort,
|
|
authenticationParameter,
|
|
reverseOStream.getByteBuffer());
|
|
|
|
ByteBuffer initResponse = acseAssociation.getAssociateResponseAPdu();
|
|
|
|
MMSpdu initiateResponseMmsPdu = new MMSpdu();
|
|
|
|
initiateResponseMmsPdu.decode(new ByteBufferInputStream(initResponse), null);
|
|
|
|
handleInitiateResponse(
|
|
initiateResponseMmsPdu,
|
|
proposedMaxPduSize,
|
|
proposedMaxServOutstandingCalling,
|
|
proposedMaxServOutstandingCalled,
|
|
proposedDataStructureNestingLevel);
|
|
} catch (IOException e) {
|
|
if (acseAssociation != null) {
|
|
acseAssociation.close();
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private void handleInitiateResponse(
|
|
MMSpdu responsePdu,
|
|
int proposedMaxPduSize,
|
|
int proposedMaxServOutstandingCalling,
|
|
int proposedMaxServOutstandingCalled,
|
|
int proposedDataStructureNestingLevel)
|
|
throws IOException {
|
|
|
|
if (responsePdu.getInitiateErrorPDU() != null) {
|
|
throw new IOException(
|
|
"Got response error of class: " + responsePdu.getInitiateErrorPDU().getErrorClass());
|
|
}
|
|
|
|
if (responsePdu.getInitiateResponsePDU() == null) {
|
|
acseAssociation.disconnect();
|
|
throw new IOException("Error decoding InitiateResponse Pdu");
|
|
}
|
|
|
|
InitiateResponsePDU initiateResponsePdu = responsePdu.getInitiateResponsePDU();
|
|
|
|
if (initiateResponsePdu.getLocalDetailCalled() != null) {
|
|
negotiatedMaxPduSize = initiateResponsePdu.getLocalDetailCalled().intValue();
|
|
}
|
|
|
|
int negotiatedMaxServOutstandingCalling =
|
|
initiateResponsePdu.getNegotiatedMaxServOutstandingCalling().intValue();
|
|
int negotiatedMaxServOutstandingCalled =
|
|
initiateResponsePdu.getNegotiatedMaxServOutstandingCalled().intValue();
|
|
|
|
int negotiatedDataStructureNestingLevel;
|
|
if (initiateResponsePdu.getNegotiatedDataStructureNestingLevel() != null) {
|
|
negotiatedDataStructureNestingLevel =
|
|
initiateResponsePdu.getNegotiatedDataStructureNestingLevel().intValue();
|
|
} else {
|
|
negotiatedDataStructureNestingLevel = proposedDataStructureNestingLevel;
|
|
}
|
|
|
|
if (negotiatedMaxPduSize < ClientSap.MINIMUM_MMS_PDU_SIZE
|
|
|| negotiatedMaxPduSize > proposedMaxPduSize
|
|
|| negotiatedMaxServOutstandingCalling > proposedMaxServOutstandingCalling
|
|
|| negotiatedMaxServOutstandingCalling < 0
|
|
|| negotiatedMaxServOutstandingCalled > proposedMaxServOutstandingCalled
|
|
|| negotiatedMaxServOutstandingCalled < 0
|
|
|| negotiatedDataStructureNestingLevel > proposedDataStructureNestingLevel
|
|
|| negotiatedDataStructureNestingLevel < 0) {
|
|
acseAssociation.disconnect();
|
|
throw new IOException("Error negotiating parameters");
|
|
}
|
|
|
|
int version =
|
|
initiateResponsePdu.getInitResponseDetail().getNegotiatedVersionNumber().intValue();
|
|
if (version != 1) {
|
|
throw new IOException("Unsupported version number was negotiated.");
|
|
}
|
|
|
|
byte[] servicesSupported =
|
|
initiateResponsePdu.getInitResponseDetail().getServicesSupportedCalled().value;
|
|
if ((servicesSupported[0] & 0x40) != 0x40) {
|
|
throw new IOException("Obligatory services are not supported by the server.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the server model instead of retrieving it from the server device.
|
|
*
|
|
* @param model the server model
|
|
*/
|
|
public void setServerModel(ServerModel model) {
|
|
this.serverModel = model;
|
|
}
|
|
|
|
/**
|
|
* Triggers all GetDirectory and GetDefinition ACSI services needed to get the complete server
|
|
* model. Because in MMS SubDataObjects cannot be distinguished from Constructed Data Attributes
|
|
* they will always be represented as Constructed Data Attributes in the returned model.
|
|
*
|
|
* @return the ServerModel that is the root node of the complete server model.
|
|
* @throws ServiceError if a ServiceError occurs while calling any of the ASCI services.
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public ServerModel retrieveModel() throws ServiceError, IOException {
|
|
|
|
List<String> ldNames = retrieveLogicalDevices();
|
|
List<List<String>> lnNames = new ArrayList<>(ldNames.size());
|
|
|
|
for (int i = 0; i < ldNames.size(); i++) {
|
|
lnNames.add(retrieveLogicalNodeNames(ldNames.get(i)));
|
|
}
|
|
List<LogicalDevice> lds = new ArrayList<>();
|
|
for (int i = 0; i < ldNames.size(); i++) {
|
|
List<LogicalNode> lns = new ArrayList<>();
|
|
for (int j = 0; j < lnNames.get(i).size(); j++) {
|
|
lns.add(
|
|
retrieveDataDefinitions(
|
|
new ObjectReference(ldNames.get(i) + "/" + lnNames.get(i).get(j))));
|
|
}
|
|
lds.add(new LogicalDevice(new ObjectReference(ldNames.get(i)), lns));
|
|
}
|
|
|
|
serverModel = new ServerModel(lds, null);
|
|
|
|
updateDataSets();
|
|
|
|
return serverModel;
|
|
}
|
|
|
|
private List<String> retrieveLogicalDevices() throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructGetServerDirectoryRequest();
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
return decodeGetServerDirectoryResponse(confirmedServiceResponse);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructGetServerDirectoryRequest() {
|
|
ObjectClass objectClass = new ObjectClass();
|
|
objectClass.setBasicObjectClass(new BerInteger(9));
|
|
|
|
GetNameListRequest.ObjectScope objectScope = new GetNameListRequest.ObjectScope();
|
|
objectScope.setVmdSpecific(new BerNull());
|
|
|
|
GetNameListRequest getNameListRequest = new GetNameListRequest();
|
|
getNameListRequest.setObjectClass(objectClass);
|
|
getNameListRequest.setObjectScope(objectScope);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setGetNameList(getNameListRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private List<String> decodeGetServerDirectoryResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse) throws ServiceError {
|
|
|
|
if (confirmedServiceResponse.getGetNameList() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding Get Server Directory Response Pdu");
|
|
}
|
|
|
|
List<Identifier> identifiers =
|
|
confirmedServiceResponse.getGetNameList().getListOfIdentifier().getIdentifier();
|
|
ArrayList<String> objectRefs = new ArrayList<>(); // ObjectReference[identifiers.size()];
|
|
|
|
for (BerVisibleString identifier : identifiers) {
|
|
objectRefs.add(identifier.toString());
|
|
}
|
|
|
|
return objectRefs;
|
|
}
|
|
|
|
private List<String> retrieveLogicalNodeNames(String ld) throws ServiceError, IOException {
|
|
List<String> lns = new LinkedList<>();
|
|
String continueAfterRef = "";
|
|
do {
|
|
ConfirmedServiceRequest serviceRequest =
|
|
constructGetDirectoryRequest(ld, continueAfterRef, true);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
continueAfterRef = decodeGetDirectoryResponse(confirmedServiceResponse, lns);
|
|
|
|
} while (continueAfterRef != "");
|
|
return lns;
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructGetDirectoryRequest(
|
|
String ldRef, String continueAfter, boolean logicalDevice) {
|
|
|
|
ObjectClass objectClass = new ObjectClass();
|
|
|
|
if (logicalDevice) {
|
|
objectClass.setBasicObjectClass(new BerInteger(0));
|
|
} else { // for data sets
|
|
objectClass.setBasicObjectClass(new BerInteger(2));
|
|
}
|
|
|
|
GetNameListRequest getNameListRequest = null;
|
|
|
|
ObjectScope objectScopeChoiceType = new ObjectScope();
|
|
objectScopeChoiceType.setDomainSpecific(new Identifier(ldRef.getBytes()));
|
|
|
|
getNameListRequest = new GetNameListRequest();
|
|
getNameListRequest.setObjectClass(objectClass);
|
|
getNameListRequest.setObjectScope(objectScopeChoiceType);
|
|
if (continueAfter != "") {
|
|
getNameListRequest.setContinueAfter(new Identifier(continueAfter.getBytes()));
|
|
}
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setGetNameList(getNameListRequest);
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
/**
|
|
* Decodes an MMS response which contains the structure of a LD and its LNs including names of
|
|
* DOs.
|
|
*/
|
|
private String decodeGetDirectoryResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, List<String> lns) throws ServiceError {
|
|
|
|
if (confirmedServiceResponse.getGetNameList() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"decodeGetLDDirectoryResponse: Error decoding server response");
|
|
}
|
|
|
|
GetNameListResponse getNameListResponse = confirmedServiceResponse.getGetNameList();
|
|
|
|
List<Identifier> identifiers = getNameListResponse.getListOfIdentifier().getIdentifier();
|
|
|
|
if (identifiers.size() == 0) {
|
|
throw new ServiceError(
|
|
ServiceError.INSTANCE_NOT_AVAILABLE,
|
|
"decodeGetLDDirectoryResponse: Instance not available");
|
|
}
|
|
|
|
BerVisibleString identifier = null;
|
|
Iterator<Identifier> it = identifiers.iterator();
|
|
|
|
String idString;
|
|
|
|
while (it.hasNext()) {
|
|
identifier = it.next();
|
|
idString = identifier.toString();
|
|
|
|
if (idString.indexOf('$') == -1) {
|
|
lns.add(idString);
|
|
}
|
|
}
|
|
|
|
if (getNameListResponse.getMoreFollows() != null
|
|
&& getNameListResponse.getMoreFollows().value == false) {
|
|
return "";
|
|
} else {
|
|
return identifier.toString();
|
|
}
|
|
}
|
|
|
|
private LogicalNode retrieveDataDefinitions(ObjectReference lnRef)
|
|
throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructGetDataDefinitionRequest(lnRef);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
return decodeGetDataDefinitionResponse(confirmedServiceResponse, lnRef);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructGetDataDefinitionRequest(ObjectReference lnRef) {
|
|
|
|
ObjectName.DomainSpecific domainSpec = new ObjectName.DomainSpecific();
|
|
domainSpec.setDomainID(new Identifier(lnRef.get(0).getBytes()));
|
|
domainSpec.setItemID(new Identifier(lnRef.get(1).getBytes()));
|
|
|
|
ObjectName objectName = new ObjectName();
|
|
objectName.setDomainSpecific(domainSpec);
|
|
|
|
GetVariableAccessAttributesRequest getVariableAccessAttributesRequest =
|
|
new GetVariableAccessAttributesRequest();
|
|
getVariableAccessAttributesRequest.setName(objectName);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setGetVariableAccessAttributes(getVariableAccessAttributesRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private LogicalNode decodeGetDataDefinitionResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, ObjectReference lnRef)
|
|
throws ServiceError {
|
|
|
|
return DataDefinitionResParser.parseGetDataDefinitionResponse(confirmedServiceResponse, lnRef);
|
|
}
|
|
|
|
/**
|
|
* The implementation of the GetDataValues ACSI service. Will send an MMS read request for the
|
|
* given model node. After a successful return, the Basic Data Attributes of the passed model node
|
|
* will contain the values read. If one of the Basic Data Attributes cannot be read then none of
|
|
* the values will be read and a <code>ServiceError</code> will be thrown.
|
|
*
|
|
* @param modelNode the functionally constrained model node that is to be read.
|
|
* @throws ServiceError if a ServiceError is returned by the server.
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public void getDataValues(FcModelNode modelNode) throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructGetDataValuesRequest(modelNode);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
decodeGetDataValuesResponse(confirmedServiceResponse, modelNode);
|
|
}
|
|
|
|
private boolean decodeGetFileDirectoryResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, List<FileInformation> files)
|
|
throws ServiceError {
|
|
if (confirmedServiceResponse.getFileDirectory() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding GetFileDirectoryResponsePdu");
|
|
}
|
|
|
|
FileDirectoryResponse fileDirectoryRes = confirmedServiceResponse.getFileDirectory();
|
|
|
|
List<DirectoryEntry> entries = fileDirectoryRes.getListOfDirectoryEntry().getDirectoryEntry();
|
|
|
|
for (DirectoryEntry entry : entries) {
|
|
List<BerGraphicString> graphicStrings = entry.getFileName().getBerGraphicString();
|
|
|
|
StringBuilder filename = new StringBuilder();
|
|
|
|
for (BerGraphicString bgs : graphicStrings) {
|
|
filename.append(bgs.toString());
|
|
}
|
|
|
|
long fileSize = entry.getFileAttributes().getSizeOfFile().longValue();
|
|
|
|
Calendar lastModified = null;
|
|
|
|
try {
|
|
|
|
if (entry.getFileAttributes().getLastModified() != null) {
|
|
lastModified = entry.getFileAttributes().getLastModified().asCalendar();
|
|
}
|
|
|
|
} catch (ParseException e) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding GetFileDirectoryResponsePdu");
|
|
}
|
|
|
|
FileInformation fileInfo = new FileInformation(filename.toString(), fileSize, lastModified);
|
|
|
|
files.add(fileInfo);
|
|
}
|
|
|
|
boolean moreFollows =
|
|
(fileDirectoryRes.getMoreFollows() != null) && fileDirectoryRes.getMoreFollows().value;
|
|
|
|
return moreFollows;
|
|
}
|
|
|
|
/**
|
|
* Read the file directory of the server
|
|
*
|
|
* @param directoryName name of a directory or empty string for the root directory
|
|
* @return the list of available
|
|
* @throws ServiceError if a ServiceError is returned by the server or parsing of response failed.
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public List<FileInformation> getFileDirectory(String directoryName)
|
|
throws ServiceError, IOException {
|
|
List<FileInformation> files = new LinkedList<>();
|
|
|
|
boolean moreFollows = true;
|
|
|
|
String continueAfter = null;
|
|
|
|
while (moreFollows) {
|
|
|
|
FileDirectoryRequest fileDirectoryRequest = new FileDirectoryRequest();
|
|
|
|
BerGraphicString berGraphicString = new BerGraphicString(directoryName.getBytes());
|
|
|
|
FileName fileSpecification = new FileName();
|
|
fileSpecification.getBerGraphicString().add(berGraphicString);
|
|
|
|
fileDirectoryRequest.setFileSpecification(fileSpecification);
|
|
|
|
if (continueAfter != null) {
|
|
FileName continueAfterSpecification = new FileName();
|
|
|
|
continueAfterSpecification
|
|
.getBerGraphicString()
|
|
.add(new BerGraphicString(continueAfter.getBytes()));
|
|
|
|
fileDirectoryRequest.setContinueAfter(continueAfterSpecification);
|
|
}
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setFileDirectory(fileDirectoryRequest);
|
|
|
|
ConfirmedServiceResponse confirmedServiceResponse =
|
|
encodeWriteReadDecode(confirmedServiceRequest);
|
|
|
|
moreFollows = decodeGetFileDirectoryResponse(confirmedServiceResponse, files);
|
|
|
|
if (moreFollows) {
|
|
continueAfter = files.get(files.size() - 1).getFilename();
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Delete a file from the server
|
|
*
|
|
* @param filename name of the file to delete
|
|
* @throws ServiceError if a ServiceError is returned by the server
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public void deleteFile(String filename) throws ServiceError, IOException {
|
|
FileDeleteRequest fileDeleteRequest = new FileDeleteRequest();
|
|
|
|
fileDeleteRequest.getBerGraphicString().add(new BerGraphicString(filename.getBytes()));
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setFileDelete(fileDeleteRequest);
|
|
|
|
ConfirmedServiceResponse confirmedServiceResponse =
|
|
encodeWriteReadDecode(confirmedServiceRequest);
|
|
|
|
if (confirmedServiceResponse.getFileDelete() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding DeleteFileResponsePdu");
|
|
}
|
|
}
|
|
|
|
private Integer32 openFile(String filename) throws ServiceError, IOException {
|
|
FileOpenRequest fileOpenRequest = new FileOpenRequest();
|
|
|
|
FileName fileSpecification = new FileName();
|
|
fileSpecification.getBerGraphicString().add(new BerGraphicString(filename.getBytes()));
|
|
|
|
fileOpenRequest.setFileName(fileSpecification);
|
|
fileOpenRequest.setInitialPosition(new Unsigned32(0));
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setFileOpen(fileOpenRequest);
|
|
|
|
ConfirmedServiceResponse confirmedServiceResponse =
|
|
encodeWriteReadDecode(confirmedServiceRequest);
|
|
|
|
if (confirmedServiceResponse.getFileOpen() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding FileOpenResponsePdu");
|
|
}
|
|
|
|
Integer32 frsmId = confirmedServiceResponse.getFileOpen().getFrsmID();
|
|
|
|
return frsmId;
|
|
}
|
|
|
|
private boolean readNextFileDataBlock(Integer32 frsmId, GetFileListener listener)
|
|
throws ServiceError, IOException {
|
|
FileReadRequest fileReadRequest = new FileReadRequest(frsmId.longValue());
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setFileRead(fileReadRequest);
|
|
|
|
ConfirmedServiceResponse confirmedServiceResponse =
|
|
encodeWriteReadDecode(confirmedServiceRequest);
|
|
|
|
if (confirmedServiceResponse.getFileRead() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding FileReadResponsePdu");
|
|
}
|
|
|
|
byte[] fileData = confirmedServiceResponse.getFileRead().getFileData().value;
|
|
|
|
boolean moreFollows = true;
|
|
|
|
if (confirmedServiceResponse.getFileRead().getMoreFollows() != null) {
|
|
moreFollows = confirmedServiceResponse.getFileRead().getMoreFollows().value;
|
|
}
|
|
|
|
if (listener != null) {
|
|
boolean continueRead = listener.dataReceived(fileData, moreFollows);
|
|
|
|
if (moreFollows == true) {
|
|
moreFollows = continueRead;
|
|
}
|
|
}
|
|
|
|
return moreFollows;
|
|
}
|
|
|
|
private void closeFile(Integer32 frsmId) throws ServiceError, IOException {
|
|
FileCloseRequest fileCloseRequest = new FileCloseRequest(frsmId.longValue());
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setFileClose(fileCloseRequest);
|
|
|
|
ConfirmedServiceResponse confirmedServiceResponse =
|
|
encodeWriteReadDecode(confirmedServiceRequest);
|
|
|
|
if (confirmedServiceResponse.getFileClose() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding FileCloseResponsePdu");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a file from the server
|
|
*
|
|
* @param filename name of the file to delete
|
|
* @param listener callback handler to receive fall data
|
|
* @throws ServiceError if a ServiceError is returned by the server
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public void getFile(String filename, GetFileListener listener) throws ServiceError, IOException {
|
|
Integer32 frsmId = openFile(filename);
|
|
|
|
boolean moreFollows = true;
|
|
|
|
while (moreFollows) {
|
|
moreFollows = readNextFileDataBlock(frsmId, listener);
|
|
}
|
|
|
|
closeFile(frsmId);
|
|
}
|
|
|
|
/**
|
|
* Will update all data inside the model except for control variables (those that have FC=CO).
|
|
* Control variables are not meant to be read. Update is done by calling getDataValues on the
|
|
* FCDOs below the Logical Nodes.
|
|
*
|
|
* @throws ServiceError if a ServiceError is returned by the server.
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public void getAllDataValues() throws ServiceError, IOException {
|
|
for (ModelNode logicalDevice : serverModel.getChildren()) {
|
|
for (ModelNode logicalNode : logicalDevice.getChildren()) {
|
|
for (ModelNode dataObject : logicalNode.getChildren()) {
|
|
FcModelNode fcdo = (FcModelNode) dataObject;
|
|
if (fcdo.getFc() != Fc.CO && fcdo.getFc() != Fc.SE) {
|
|
try {
|
|
getDataValues(fcdo);
|
|
} catch (ServiceError e) {
|
|
throw new ServiceError(
|
|
e.getErrorCode(),
|
|
"service error retrieving "
|
|
+ fcdo.getReference()
|
|
+ "["
|
|
+ fcdo.getFc()
|
|
+ "]"
|
|
+ ", "
|
|
+ e.getMessage(),
|
|
e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructGetDataValuesRequest(FcModelNode modelNode) {
|
|
VariableAccessSpecification varAccessSpec = constructVariableAccessSpecification(modelNode);
|
|
|
|
ReadRequest readRequest = new ReadRequest();
|
|
readRequest.setVariableAccessSpecification(varAccessSpec);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setRead(readRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private void decodeGetDataValuesResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, ModelNode modelNode) throws ServiceError {
|
|
|
|
if (confirmedServiceResponse.getRead() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding GetDataValuesReponsePdu");
|
|
}
|
|
|
|
List<AccessResult> listOfAccessResults =
|
|
confirmedServiceResponse.getRead().getListOfAccessResult().getAccessResult();
|
|
|
|
if (listOfAccessResults.size() != 1) {
|
|
throw new ServiceError(
|
|
ServiceError.PARAMETER_VALUE_INAPPROPRIATE, "Multiple results received.");
|
|
}
|
|
|
|
AccessResult accRes = listOfAccessResults.get(0);
|
|
|
|
if (accRes.getFailure() != null) {
|
|
throw mmsDataAccessErrorToServiceError(accRes.getFailure());
|
|
}
|
|
modelNode.setValueFromMmsDataObj(accRes.getSuccess());
|
|
}
|
|
|
|
/**
|
|
* The implementation of the SetDataValues ACSI service. Will send an MMS write request with the
|
|
* values of all Basic Data Attributes of the given model node. Will simply return if all values
|
|
* have been successfully written. If one of the Basic Data Attributes could not be written then a
|
|
* <code>ServiceError</code> will be thrown. In this case it is not possible to find out which of
|
|
* several Basic Data Attributes could not be written.
|
|
*
|
|
* @param modelNode the functionally constrained model node that is to be written.
|
|
* @throws ServiceError if a ServiceError is returned by the server.
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public void setDataValues(FcModelNode modelNode) throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructSetDataValuesRequest(modelNode);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
decodeSetDataValuesResponse(confirmedServiceResponse);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructSetDataValuesRequest(FcModelNode modelNode)
|
|
throws ServiceError {
|
|
|
|
VariableAccessSpecification variableAccessSpecification =
|
|
constructVariableAccessSpecification(modelNode);
|
|
|
|
ListOfData listOfData = new ListOfData();
|
|
List<Data> dataList = listOfData.getData();
|
|
dataList.add(modelNode.getMmsDataObj());
|
|
|
|
WriteRequest writeRequest = new WriteRequest();
|
|
writeRequest.setListOfData(listOfData);
|
|
writeRequest.setVariableAccessSpecification(variableAccessSpecification);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setWrite(writeRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private VariableAccessSpecification constructVariableAccessSpecification(FcModelNode modelNode) {
|
|
VariableDefs listOfVariable = new VariableDefs();
|
|
|
|
List<VariableDefs.SEQUENCE> variableDefsSeqOf = listOfVariable.getSEQUENCE();
|
|
variableDefsSeqOf.add(modelNode.getMmsVariableDef());
|
|
|
|
VariableAccessSpecification variableAccessSpecification = new VariableAccessSpecification();
|
|
variableAccessSpecification.setListOfVariable(listOfVariable);
|
|
|
|
return variableAccessSpecification;
|
|
}
|
|
|
|
private void decodeSetDataValuesResponse(ConfirmedServiceResponse confirmedServiceResponse)
|
|
throws ServiceError {
|
|
|
|
WriteResponse writeResponse = confirmedServiceResponse.getWrite();
|
|
|
|
if (writeResponse == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"SetDataValuesResponse: improper response");
|
|
}
|
|
|
|
WriteResponse.CHOICE subChoice = writeResponse.getCHOICE().get(0);
|
|
|
|
if (subChoice.getFailure() != null) {
|
|
throw mmsDataAccessErrorToServiceError(subChoice.getFailure());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function will get the definition of all persistent DataSets from the server and update the
|
|
* DataSets in the ServerModel that were returned by the retrieveModel() or getModelFromSclFile()
|
|
* functions. It will delete DataSets that have been deleted since the last update and add any new
|
|
* DataSets
|
|
*
|
|
* @throws ServiceError if a ServiceError is returned by the server.
|
|
* @throws IOException if a fatal association error occurs. The association object will be closed
|
|
* and can no longer be used after this exception is thrown.
|
|
*/
|
|
public void updateDataSets() throws ServiceError, IOException {
|
|
|
|
if (serverModel == null) {
|
|
throw new IllegalStateException(
|
|
"Before calling this function you have to get the ServerModel using the retrieveModel() function");
|
|
}
|
|
|
|
Collection<ModelNode> lds = serverModel.getChildren();
|
|
|
|
for (ModelNode ld : lds) {
|
|
ConfirmedServiceRequest serviceRequest =
|
|
constructGetDirectoryRequest(ld.getName(), "", false);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
decodeAndRetrieveDsNamesAndDefinitions(confirmedServiceResponse, (LogicalDevice) ld);
|
|
}
|
|
}
|
|
|
|
private void decodeAndRetrieveDsNamesAndDefinitions(
|
|
ConfirmedServiceResponse confirmedServiceResponse, LogicalDevice ld)
|
|
throws ServiceError, IOException {
|
|
|
|
if (confirmedServiceResponse.getGetNameList() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"decodeGetDataSetResponse: Error decoding server response");
|
|
}
|
|
|
|
GetNameListResponse getNameListResponse = confirmedServiceResponse.getGetNameList();
|
|
|
|
List<Identifier> identifiers = getNameListResponse.getListOfIdentifier().getIdentifier();
|
|
|
|
if (identifiers.size() == 0) {
|
|
return;
|
|
}
|
|
|
|
for (Identifier identifier : identifiers) {
|
|
// TODO delete DataSets that no longer exist
|
|
getDataSetDirectory(identifier, ld);
|
|
}
|
|
|
|
if (getNameListResponse.getMoreFollows() != null
|
|
&& getNameListResponse.getMoreFollows().value == true) {
|
|
throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT);
|
|
}
|
|
}
|
|
|
|
private void getDataSetDirectory(Identifier dsId, LogicalDevice ld)
|
|
throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructGetDataSetDirectoryRequest(dsId, ld);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
decodeGetDataSetDirectoryResponse(confirmedServiceResponse, dsId, ld);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructGetDataSetDirectoryRequest(
|
|
Identifier dsId, LogicalDevice ld) throws ServiceError {
|
|
ObjectName.DomainSpecific domainSpecificObjectName = new ObjectName.DomainSpecific();
|
|
domainSpecificObjectName.setDomainID(new Identifier(ld.getName().getBytes()));
|
|
domainSpecificObjectName.setItemID(dsId);
|
|
|
|
GetNamedVariableListAttributesRequest dataSetObj = new GetNamedVariableListAttributesRequest();
|
|
dataSetObj.setDomainSpecific(domainSpecificObjectName);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setGetNamedVariableListAttributes(dataSetObj);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private void decodeGetDataSetDirectoryResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, BerVisibleString dsId, LogicalDevice ld)
|
|
throws ServiceError {
|
|
|
|
if (confirmedServiceResponse.getGetNamedVariableListAttributes() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"decodeGetDataSetDirectoryResponse: Error decoding server response");
|
|
}
|
|
|
|
GetNamedVariableListAttributesResponse getNamedVariableListAttResponse =
|
|
confirmedServiceResponse.getGetNamedVariableListAttributes();
|
|
boolean deletable = getNamedVariableListAttResponse.getMmsDeletable().value;
|
|
List<VariableDefs.SEQUENCE> variables =
|
|
getNamedVariableListAttResponse.getListOfVariable().getSEQUENCE();
|
|
|
|
if (variables.size() == 0) {
|
|
throw new ServiceError(
|
|
ServiceError.INSTANCE_NOT_AVAILABLE,
|
|
"decodeGetDataSetDirectoryResponse: Instance not available");
|
|
}
|
|
|
|
List<FcModelNode> dsMems = new ArrayList<>();
|
|
|
|
for (VariableDefs.SEQUENCE variableDef : variables) {
|
|
|
|
FcModelNode member;
|
|
// TODO remove this try catch statement once all possible FCs are
|
|
// supported
|
|
// it is only there so that Functional Constraints such as GS will
|
|
// be ignored and DataSet cotaining elements with these FCs are
|
|
// ignored and not created.
|
|
try {
|
|
member = serverModel.getNodeFromVariableDef(variableDef);
|
|
} catch (ServiceError e) {
|
|
return;
|
|
}
|
|
if (member == null) {
|
|
throw new ServiceError(
|
|
ServiceError.INSTANCE_NOT_AVAILABLE,
|
|
"decodeGetDataSetDirectoryResponse: data set memeber does not exist, you might have to call retrieveModel first");
|
|
}
|
|
dsMems.add(member);
|
|
}
|
|
|
|
String dsObjRef = ld.getName() + "/" + dsId.toString().replace('$', '.');
|
|
|
|
DataSet dataSet = new DataSet(dsObjRef, dsMems, deletable);
|
|
|
|
if (ld.getChild(dsId.toString().substring(0, dsId.toString().indexOf('$'))) == null) {
|
|
throw new ServiceError(
|
|
ServiceError.INSTANCE_NOT_AVAILABLE,
|
|
"decodeGetDataSetDirectoryResponse: LN for returned DataSet is not available");
|
|
}
|
|
|
|
DataSet existingDs = serverModel.getDataSet(dsObjRef);
|
|
if (existingDs == null) {
|
|
serverModel.addDataSet(dataSet);
|
|
} else if (!existingDs.isDeletable()) {
|
|
return;
|
|
} else {
|
|
serverModel.removeDataSet(dsObjRef);
|
|
serverModel.addDataSet(dataSet);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client should create the data set first and add it to either the non-persistent list or to
|
|
* the model. Then it should call this method for creation on the server side
|
|
*
|
|
* @param dataSet the data set to be created on the server side
|
|
* @throws ServiceError if a ServiceError is returned by the server.
|
|
* @throws IOException if a fatal IO error occurs. The association object will be closed and can
|
|
* no longer be used after this exception is thrown.
|
|
*/
|
|
public void createDataSet(DataSet dataSet) throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructCreateDataSetRequest(dataSet);
|
|
encodeWriteReadDecode(serviceRequest);
|
|
handleCreateDataSetResponse(dataSet);
|
|
}
|
|
|
|
/**
|
|
* dsRef = either LD/LN.DataSetName (persistent) or @DataSetname (non-persistent) Names in
|
|
* dsMemberRef should be in the form: LD/LNName.DoName or LD/LNName.DoName.DaName
|
|
*/
|
|
private ConfirmedServiceRequest constructCreateDataSetRequest(DataSet dataSet)
|
|
throws ServiceError {
|
|
|
|
VariableDefs listOfVariable = new VariableDefs();
|
|
|
|
List<VariableDefs.SEQUENCE> variableDefs = listOfVariable.getSEQUENCE();
|
|
for (FcModelNode dsMember : dataSet) {
|
|
variableDefs.add(dsMember.getMmsVariableDef());
|
|
}
|
|
|
|
DefineNamedVariableListRequest createDSRequest = new DefineNamedVariableListRequest();
|
|
createDSRequest.setVariableListName(dataSet.getMmsObjectName());
|
|
createDSRequest.setListOfVariable(listOfVariable);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setDefineNamedVariableList(createDSRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private void handleCreateDataSetResponse(DataSet dataSet) throws ServiceError {
|
|
serverModel.addDataSet(dataSet);
|
|
}
|
|
|
|
public void deleteDataSet(DataSet dataSet) throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructDeleteDataSetRequest(dataSet);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
decodeDeleteDataSetResponse(confirmedServiceResponse, dataSet);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructDeleteDataSetRequest(DataSet dataSet)
|
|
throws ServiceError {
|
|
|
|
ListOfVariableListName listOfVariableListName = new ListOfVariableListName();
|
|
|
|
List<ObjectName> objectList = listOfVariableListName.getObjectName();
|
|
objectList.add(dataSet.getMmsObjectName());
|
|
|
|
DeleteNamedVariableListRequest requestDeleteDS = new DeleteNamedVariableListRequest();
|
|
requestDeleteDS.setListOfVariableListName(listOfVariableListName);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setDeleteNamedVariableList(requestDeleteDS);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private void decodeDeleteDataSetResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, DataSet dataSet) throws ServiceError {
|
|
|
|
if (confirmedServiceResponse.getDeleteNamedVariableList() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"decodeDeleteDataSetResponse: Error decoding server response");
|
|
}
|
|
|
|
DeleteNamedVariableListResponse deleteNamedVariableListResponse =
|
|
confirmedServiceResponse.getDeleteNamedVariableList();
|
|
|
|
if (deleteNamedVariableListResponse.getNumberDeleted().intValue() != 1) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, "number deleted not 1");
|
|
}
|
|
|
|
if (serverModel.removeDataSet(dataSet.getReferenceStr()) == null) {
|
|
throw new ServiceError(ServiceError.UNKNOWN, "unable to delete dataset locally");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The implementation of the GetDataSetValues ACSI service. After a successful return, the Basic
|
|
* Data Attributes of the data set members will contain the values read. If one of the data set
|
|
* members could not be read, this will be indicated in the returned list. The returned list will
|
|
* have the same size as the member list of the data set. For each member it will contain <code>
|
|
* null</code> if reading was successful and a ServiceError if reading of this member failed.
|
|
*
|
|
* @param dataSet the DataSet that is to be read.
|
|
* @return a list indicating ServiceErrors that may have occurred.
|
|
* @throws IOException if a fatal IO error occurs. The association object will be closed and can
|
|
* no longer be used after this exception is thrown.
|
|
*/
|
|
public List<ServiceError> getDataSetValues(DataSet dataSet) throws IOException {
|
|
|
|
ConfirmedServiceResponse confirmedServiceResponse;
|
|
try {
|
|
ConfirmedServiceRequest serviceRequest = constructGetDataSetValuesRequest(dataSet);
|
|
confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
} catch (ServiceError e) {
|
|
int dataSetSize = dataSet.getMembers().size();
|
|
List<ServiceError> serviceErrors = new ArrayList<>(dataSetSize);
|
|
for (int i = 0; i < dataSetSize; i++) {
|
|
serviceErrors.add(e);
|
|
}
|
|
return serviceErrors;
|
|
}
|
|
return decodeGetDataSetValuesResponse(confirmedServiceResponse, dataSet);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructGetDataSetValuesRequest(DataSet dataSet)
|
|
throws ServiceError {
|
|
|
|
VariableAccessSpecification varAccSpec = new VariableAccessSpecification();
|
|
varAccSpec.setVariableListName(dataSet.getMmsObjectName());
|
|
|
|
ReadRequest getDataSetValuesRequest = new ReadRequest();
|
|
getDataSetValuesRequest.setSpecificationWithResult(new BerBoolean(true));
|
|
getDataSetValuesRequest.setVariableAccessSpecification(varAccSpec);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setRead(getDataSetValuesRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private List<ServiceError> decodeGetDataSetValuesResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse, DataSet ds) {
|
|
|
|
int dataSetSize = ds.getMembers().size();
|
|
List<ServiceError> serviceErrors = new ArrayList<>(dataSetSize);
|
|
|
|
if (confirmedServiceResponse.getRead() == null) {
|
|
ServiceError serviceError =
|
|
new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding GetDataValuesReponsePdu");
|
|
for (int i = 0; i < dataSetSize; i++) {
|
|
serviceErrors.add(serviceError);
|
|
}
|
|
return serviceErrors;
|
|
}
|
|
|
|
ReadResponse readResponse = confirmedServiceResponse.getRead();
|
|
List<AccessResult> listOfAccessResults = readResponse.getListOfAccessResult().getAccessResult();
|
|
|
|
if (listOfAccessResults.size() != ds.getMembers().size()) {
|
|
ServiceError serviceError =
|
|
new ServiceError(
|
|
ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
|
|
"Number of AccessResults does not match the number of DataSet members.");
|
|
for (int i = 0; i < dataSetSize; i++) {
|
|
serviceErrors.add(serviceError);
|
|
}
|
|
return serviceErrors;
|
|
}
|
|
|
|
Iterator<AccessResult> accessResultIterator = listOfAccessResults.iterator();
|
|
|
|
for (FcModelNode dsMember : ds) {
|
|
AccessResult accessResult = accessResultIterator.next();
|
|
if (accessResult.getSuccess() != null) {
|
|
try {
|
|
dsMember.setValueFromMmsDataObj(accessResult.getSuccess());
|
|
} catch (ServiceError e) {
|
|
serviceErrors.add(e);
|
|
}
|
|
serviceErrors.add(null);
|
|
} else {
|
|
serviceErrors.add(mmsDataAccessErrorToServiceError(accessResult.getFailure()));
|
|
}
|
|
}
|
|
|
|
return serviceErrors;
|
|
}
|
|
|
|
public List<ServiceError> setDataSetValues(DataSet dataSet) throws ServiceError, IOException {
|
|
ConfirmedServiceRequest serviceRequest = constructSetDataSetValues(dataSet);
|
|
ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
|
|
return decodeSetDataSetValuesResponse(confirmedServiceResponse);
|
|
}
|
|
|
|
private ConfirmedServiceRequest constructSetDataSetValues(DataSet dataSet) throws ServiceError {
|
|
VariableAccessSpecification varAccessSpec = new VariableAccessSpecification();
|
|
varAccessSpec.setVariableListName(dataSet.getMmsObjectName());
|
|
|
|
ListOfData listOfData = new ListOfData();
|
|
List<Data> dataList = listOfData.getData();
|
|
|
|
for (ModelNode member : dataSet) {
|
|
dataList.add(member.getMmsDataObj());
|
|
}
|
|
|
|
WriteRequest writeRequest = new WriteRequest();
|
|
writeRequest.setVariableAccessSpecification(varAccessSpec);
|
|
writeRequest.setListOfData(listOfData);
|
|
|
|
ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
|
|
confirmedServiceRequest.setWrite(writeRequest);
|
|
|
|
return confirmedServiceRequest;
|
|
}
|
|
|
|
private List<ServiceError> decodeSetDataSetValuesResponse(
|
|
ConfirmedServiceResponse confirmedServiceResponse) throws ServiceError {
|
|
|
|
if (confirmedServiceResponse.getWrite() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"Error decoding SetDataSetValuesReponsePdu");
|
|
}
|
|
|
|
WriteResponse writeResponse = confirmedServiceResponse.getWrite();
|
|
List<WriteResponse.CHOICE> writeResChoiceType = writeResponse.getCHOICE();
|
|
List<ServiceError> serviceErrors = new ArrayList<>(writeResChoiceType.size());
|
|
|
|
for (WriteResponse.CHOICE accessResult : writeResChoiceType) {
|
|
if (accessResult.getSuccess() != null) {
|
|
serviceErrors.add(null);
|
|
} else {
|
|
serviceErrors.add(mmsDataAccessErrorToServiceError(accessResult.getFailure()));
|
|
}
|
|
}
|
|
return serviceErrors;
|
|
}
|
|
|
|
public void getRcbValues(Rcb rcb) throws ServiceError, IOException {
|
|
getDataValues(rcb);
|
|
}
|
|
|
|
public void reserveUrcb(Urcb urcb) throws ServiceError, IOException {
|
|
BdaBoolean resvBda = urcb.getResv();
|
|
resvBda.setValue(true);
|
|
setDataValues(resvBda);
|
|
}
|
|
|
|
public void reserveBrcb(Brcb brcb, short resvTime) throws ServiceError, IOException {
|
|
BdaInt16 resvTmsBda = brcb.getResvTms();
|
|
resvTmsBda.setValue(resvTime);
|
|
setDataValues(resvTmsBda);
|
|
}
|
|
|
|
public void cancelUrcbReservation(Urcb urcb) throws ServiceError, IOException {
|
|
BdaBoolean resvBda = urcb.getResv();
|
|
resvBda.setValue(false);
|
|
setDataValues(resvBda);
|
|
}
|
|
|
|
public void enableReporting(Rcb rcb) throws ServiceError, IOException {
|
|
BdaBoolean rptEnaBda = rcb.getRptEna();
|
|
rptEnaBda.setValue(true);
|
|
setDataValues(rptEnaBda);
|
|
}
|
|
|
|
public void disableReporting(Rcb rcb) throws ServiceError, IOException {
|
|
BdaBoolean rptEnaBda = rcb.getRptEna();
|
|
rptEnaBda.setValue(false);
|
|
setDataValues(rptEnaBda);
|
|
}
|
|
|
|
public void startGi(Rcb rcb) throws ServiceError, IOException {
|
|
BdaBoolean rptGiBda = (BdaBoolean) rcb.getChild("GI");
|
|
rptGiBda.setValue(true);
|
|
setDataValues(rptGiBda);
|
|
}
|
|
|
|
/**
|
|
* Sets the selected values of the given report control block. Note that all these parameters may
|
|
* only be set if the RCB has been reserved but reporting has not been enabled yet.
|
|
*
|
|
* <p>The data set reference as it is set in an RCB must contain a dollar sign instead of a dot to
|
|
* separate the logical node from the data set name, e.g.: 'LDevice1/LNode$DataSetName'. Therefore
|
|
* his method will check the reference for a dot and if necessary convert it to a '$' sign before
|
|
* sending the request to the server.
|
|
*
|
|
* <p>The parameters PurgeBuf, EntryId are only applicable if the given rcb is of type BRCB.
|
|
*
|
|
* @param rcb the report control block
|
|
* @param setRptId whether to set the report ID
|
|
* @param setDatSet whether to set the data set
|
|
* @param setOptFlds whether to set the optional fields
|
|
* @param setBufTm whether to set the buffer time
|
|
* @param setTrgOps whether to set the trigger options
|
|
* @param setIntgPd whether to set the integrity period
|
|
* @param setPurgeBuf whether to set purge buffer
|
|
* @param setEntryId whether to set the entry ID
|
|
* @return a list indicating ServiceErrors that may have occurred.
|
|
* @throws IOException if a fatal IO error occurs. The association object will be closed and can
|
|
* no longer be used after this exception is thrown.
|
|
*/
|
|
public List<ServiceError> setRcbValues(
|
|
Rcb rcb,
|
|
boolean setRptId,
|
|
boolean setDatSet,
|
|
boolean setOptFlds,
|
|
boolean setBufTm,
|
|
boolean setTrgOps,
|
|
boolean setIntgPd,
|
|
boolean setPurgeBuf,
|
|
boolean setEntryId)
|
|
throws IOException {
|
|
|
|
List<FcModelNode> parametersToSet = new ArrayList<>(6);
|
|
|
|
if (setRptId == true) {
|
|
parametersToSet.add(rcb.getRptId());
|
|
}
|
|
if (setDatSet == true) {
|
|
rcb.getDatSet().setValue(rcb.getDatSet().getStringValue().replace('.', '$'));
|
|
parametersToSet.add(rcb.getDatSet());
|
|
}
|
|
if (setOptFlds == true) {
|
|
parametersToSet.add(rcb.getOptFlds());
|
|
}
|
|
if (setBufTm == true) {
|
|
parametersToSet.add(rcb.getBufTm());
|
|
}
|
|
if (setTrgOps == true) {
|
|
parametersToSet.add(rcb.getTrgOps());
|
|
}
|
|
if (setIntgPd == true) {
|
|
parametersToSet.add(rcb.getIntgPd());
|
|
}
|
|
if (rcb instanceof Brcb) {
|
|
Brcb brcb = (Brcb) rcb;
|
|
if (setPurgeBuf == true) {
|
|
parametersToSet.add(brcb.getPurgeBuf());
|
|
}
|
|
if (setEntryId == true) {
|
|
parametersToSet.add(brcb.getEntryId());
|
|
}
|
|
}
|
|
|
|
List<ServiceError> serviceErrors = new ArrayList<>(parametersToSet.size());
|
|
|
|
for (FcModelNode child : parametersToSet) {
|
|
try {
|
|
setDataValues(child);
|
|
serviceErrors.add(null);
|
|
} catch (ServiceError e) {
|
|
serviceErrors.add(e);
|
|
}
|
|
}
|
|
|
|
return serviceErrors;
|
|
}
|
|
|
|
private Report processReport(MMSpdu mmsPdu) throws ServiceError {
|
|
|
|
if (mmsPdu.getUnconfirmedPDU() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"getReport: Error decoding server response");
|
|
}
|
|
|
|
UnconfirmedPDU unconfirmedRes = mmsPdu.getUnconfirmedPDU();
|
|
|
|
if (unconfirmedRes.getService() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"getReport: Error decoding server response");
|
|
}
|
|
|
|
UnconfirmedService unconfirmedServ = unconfirmedRes.getService();
|
|
|
|
if (unconfirmedServ.getInformationReport() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"getReport: Error decoding server response");
|
|
}
|
|
|
|
List<AccessResult> listRes =
|
|
unconfirmedServ.getInformationReport().getListOfAccessResult().getAccessResult();
|
|
|
|
int index = 0;
|
|
|
|
if (listRes.get(index).getSuccess().getVisibleString() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"processReport: report does not contain RptID");
|
|
}
|
|
|
|
String rptId = listRes.get(index++).getSuccess().getVisibleString().toString();
|
|
|
|
if (listRes.get(index).getSuccess().getBitString() == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"processReport: report does not contain OptFlds");
|
|
}
|
|
|
|
BdaOptFlds optFlds = new BdaOptFlds(new ObjectReference("none"), null);
|
|
optFlds.setValue(listRes.get(index++).getSuccess().getBitString().value);
|
|
|
|
Integer sqNum = null;
|
|
if (optFlds.isSequenceNumber()) {
|
|
sqNum = listRes.get(index++).getSuccess().getUnsigned().intValue();
|
|
}
|
|
|
|
BdaEntryTime timeOfEntry = null;
|
|
if (optFlds.isReportTimestamp()) {
|
|
timeOfEntry = new BdaEntryTime(new ObjectReference("none"), null, "", false, false);
|
|
timeOfEntry.setValueFromMmsDataObj(listRes.get(index++).getSuccess());
|
|
}
|
|
|
|
String dataSetRef = null;
|
|
if (optFlds.isDataSetName()) {
|
|
dataSetRef = (listRes.get(index++).getSuccess().getVisibleString().toString());
|
|
} else {
|
|
for (Urcb urcb : serverModel.getUrcbs()) {
|
|
if ((urcb.getRptId() != null && urcb.getRptId().getStringValue().equals(rptId))
|
|
|| urcb.getReference().toString().equals(rptId)) {
|
|
dataSetRef = urcb.getDatSet().getStringValue();
|
|
break;
|
|
}
|
|
}
|
|
if (dataSetRef == null) {
|
|
for (Brcb brcb : serverModel.getBrcbs()) {
|
|
if ((brcb.getRptId() != null && brcb.getRptId().getStringValue().equals(rptId))
|
|
|| brcb.getReference().toString().equals(rptId)) {
|
|
dataSetRef = brcb.getDatSet().getStringValue();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (dataSetRef == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"unable to find RCB that matches the given RptID in the report.");
|
|
}
|
|
dataSetRef = dataSetRef.replace('$', '.');
|
|
|
|
DataSet dataSet = serverModel.getDataSet(dataSetRef);
|
|
if (dataSet == null) {
|
|
throw new ServiceError(
|
|
ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
|
|
"unable to find data set that matches the given data set reference of the report.");
|
|
}
|
|
|
|
Boolean bufOvfl = null;
|
|
if (optFlds.isBufferOverflow()) {
|
|
bufOvfl = (listRes.get(index++).getSuccess().getBool().value);
|
|
}
|
|
|
|
BdaOctetString entryId = null;
|
|
if (optFlds.isEntryId()) {
|
|
entryId = new BdaOctetString(new ObjectReference("none"), null, "", 8, false, false);
|
|
entryId.setValue(listRes.get(index++).getSuccess().getOctetString().value);
|
|
}
|
|
|
|
Long confRev = null;
|
|
if (optFlds.isConfigRevision()) {
|
|
confRev = listRes.get(index++).getSuccess().getUnsigned().longValue();
|
|
}
|
|
|
|
Integer subSqNum = null;
|
|
boolean moreSegmentsFollow = false;
|
|
if (optFlds.isSegmentation()) {
|
|
subSqNum = listRes.get(index++).getSuccess().getUnsigned().intValue();
|
|
moreSegmentsFollow = listRes.get(index++).getSuccess().getBool().value;
|
|
}
|
|
|
|
boolean[] inclusionBitString =
|
|
listRes.get(index++).getSuccess().getBitString().getValueAsBooleans();
|
|
int numMembersReported = 0;
|
|
for (boolean bit : inclusionBitString) {
|
|
if (bit) {
|
|
numMembersReported++;
|
|
}
|
|
}
|
|
|
|
if (optFlds.isDataReference()) {
|
|
// this is just to move the index to the right place
|
|
// The next part will process the changes to the values
|
|
// without the dataRefs
|
|
index += numMembersReported;
|
|
}
|
|
|
|
List<FcModelNode> reportedDataSetMembers = new ArrayList<>(numMembersReported);
|
|
int dataSetIndex = 0;
|
|
for (FcModelNode dataSetMember : dataSet.getMembers()) {
|
|
if (inclusionBitString[dataSetIndex]) {
|
|
AccessResult accessRes = listRes.get(index++);
|
|
FcModelNode dataSetMemberCopy = (FcModelNode) dataSetMember.copy();
|
|
dataSetMemberCopy.setValueFromMmsDataObj(accessRes.getSuccess());
|
|
reportedDataSetMembers.add(dataSetMemberCopy);
|
|
}
|
|
dataSetIndex++;
|
|
}
|
|
|
|
List<BdaReasonForInclusion> reasonCodes = null;
|
|
if (optFlds.isReasonForInclusion()) {
|
|
reasonCodes = new ArrayList<>(dataSet.getMembers().size());
|
|
for (int i = 0; i < dataSet.getMembers().size(); i++) {
|
|
if (inclusionBitString[i]) {
|
|
BdaReasonForInclusion reasonForInclusion = new BdaReasonForInclusion(null);
|
|
reasonCodes.add(reasonForInclusion);
|
|
byte[] reason = listRes.get(index++).getSuccess().getBitString().value;
|
|
reasonForInclusion.setValue(reason);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Report(
|
|
rptId,
|
|
sqNum,
|
|
subSqNum,
|
|
moreSegmentsFollow,
|
|
dataSetRef,
|
|
bufOvfl,
|
|
confRev,
|
|
timeOfEntry,
|
|
entryId,
|
|
inclusionBitString,
|
|
reportedDataSetMembers,
|
|
reasonCodes);
|
|
}
|
|
|
|
/**
|
|
* Performs the Select ACSI Service of the control model on the given controllable Data Object
|
|
* (DO). By selecting a controllable DO you can reserve it for exclusive control/operation. This
|
|
* service is only applicable if the ctlModel Data Attribute is set to "sbo-with-normal-security"
|
|
* (2).
|
|
*
|
|
* <p>The selection is canceled in one of the following events:
|
|
*
|
|
* <ul>
|
|
* <li>The "Cancel" ACSI service is issued.
|
|
* <li>The sboTimemout (select before operate timeout) runs out. If the given controlDataObject
|
|
* contains a sboTimeout Data Attribute it is possible to change the timeout after which the
|
|
* selection/reservation is automatically canceled by the server. Otherwise the timeout is a
|
|
* local issue of the server.
|
|
* <li>The connection to the server is closed.
|
|
* <li>An operate service failed because of some error
|
|
* <li>The sboClass is set to "operate-once" then the selection is also canceled after a
|
|
* successful operate service.
|
|
* </ul>
|
|
*
|
|
* @param controlDataObject needs to be a controllable Data Object that contains a Data Attribute
|
|
* named "SBO".
|
|
* @return false if the selection/reservation was not successful (because it is already selected
|
|
* by another client). Otherwise true is returned.
|
|
* @throws ServiceError if a ServiceError is returned by the server.
|
|
* @throws IOException if a fatal IO error occurs. The association object will be closed and can
|
|
* no longer be used after this exception is thrown.
|
|
*/
|
|
public boolean select(FcModelNode controlDataObject) throws ServiceError, IOException {
|
|
BdaVisibleString sbo;
|
|
try {
|
|
sbo = (BdaVisibleString) controlDataObject.getChild("SBO");
|
|
} catch (Exception e) {
|
|
throw new IllegalArgumentException(
|
|
"ModelNode needs to conain a child node named SBO in order to select");
|
|
}
|
|
|
|
getDataValues(sbo);
|
|
|
|
return sbo.getValue().length != 0;
|
|
}
|
|
|
|
/**
|
|
* Executes the Operate ACSI Service on the given controllable Data Object (DO). The following
|
|
* subnodes of the given control DO should be set according your needs before calling this
|
|
* function. (Note that you can probably leave most attributes with their default value):
|
|
*
|
|
* <ul>
|
|
* <li>Oper.ctlVal - has to be set to actual control value that is to be written using the
|
|
* operate service.
|
|
* <li>Oper.operTm (type: BdaTimestamp) - is an optional sub data attribute of Oper (thus it may
|
|
* not exist). If it exists it can be used to set the timestamp when the operation shall be
|
|
* performed by the server. Thus the server will delay execution of the operate command
|
|
* until the given date is reached. Can be set to an empty byte array (new byte[0]) or null
|
|
* so that the server executes the operate command immediately. This is also the default.
|
|
* <li>Oper.check (type: BdaCheck) is used to tell the server whether to perform the
|
|
* synchrocheck and interlockcheck. By default they are turned off.
|
|
* <li>Oper.orign - contains the two data attributes orCat (origin category, type: BdaInt8) and
|
|
* orIdent (origin identifier, type BdaOctetString). Origin is optionally reflected in the
|
|
* status Data Attribute controlDO.origin. By reading this data attribute other clients can
|
|
* see who executed the last operate command. The default value for orCat is 0
|
|
* ("not-supported") and the default value for orIdent is ""(the empty string).
|
|
* <li>Oper.Test (BdaBoolean) - if true this operate command is sent for test purposes only.
|
|
* Default is false.
|
|
* </ul>
|
|
*
|
|
* All other operate parameters are automatically handled by this function.
|
|
*
|
|
* @param controlDataObject needs to be a controllable Data Object that contains a Data Attribute
|
|
* named "Oper".
|
|
* @throws ServiceError if a ServiceError is returned by the server
|
|
* @throws IOException if a fatal IO error occurs. The association object will be closed and can
|
|
* no longer be used after this exception is thrown.
|
|
*/
|
|
public void operate(FcModelNode controlDataObject) throws ServiceError, IOException {
|
|
ConstructedDataAttribute oper;
|
|
try {
|
|
oper = (ConstructedDataAttribute) controlDataObject.getChild("Oper");
|
|
} catch (Exception e) {
|
|
throw new IllegalArgumentException("ModelNode needs to conain a child node named \"Oper\".");
|
|
}
|
|
|
|
((BdaInt8U) oper.getChild("ctlNum")).setValue((short) 1);
|
|
((BdaTimestamp) oper.getChild("T")).setDate(new Date(System.currentTimeMillis()));
|
|
|
|
setDataValues(oper);
|
|
}
|
|
|
|
public boolean isOpen() {
|
|
return !closed;
|
|
}
|
|
|
|
/** Will close the connection simply by closing the TCP socket. */
|
|
public void close() {
|
|
clientReceiver.close(new IOException("Connection closed by client"));
|
|
}
|
|
|
|
/** Will send a disconnect request first and then close the TCP socket. */
|
|
public void disconnect() {
|
|
clientReceiver.disconnect();
|
|
}
|
|
|
|
final class ClientReceiver extends Thread {
|
|
|
|
private final ByteBuffer pduBuffer;
|
|
private Integer expectedResponseId;
|
|
private IOException lastIOException = null;
|
|
|
|
public ClientReceiver(int maxMmsPduSize) {
|
|
pduBuffer = ByteBuffer.allocate(maxMmsPduSize + 400);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
while (true) {
|
|
|
|
pduBuffer.clear();
|
|
byte[] buffer;
|
|
try {
|
|
buffer = acseAssociation.receive(pduBuffer);
|
|
} catch (TimeoutException e) {
|
|
// Illegal state: A timeout exception was thrown.
|
|
throw new IllegalStateException();
|
|
} catch (DecodingException e) {
|
|
// Error decoding the OSI headers of the received packet
|
|
continue;
|
|
}
|
|
|
|
MMSpdu decodedResponsePdu = new MMSpdu();
|
|
try {
|
|
decodedResponsePdu.decode(new ByteArrayInputStream(buffer), null);
|
|
} catch (IOException e) {
|
|
// Error decoding the received MMS PDU
|
|
continue;
|
|
}
|
|
|
|
if (decodedResponsePdu.getUnconfirmedPDU() != null) {
|
|
if (decodedResponsePdu
|
|
.getUnconfirmedPDU()
|
|
.getService()
|
|
.getInformationReport()
|
|
.getVariableAccessSpecification()
|
|
.getListOfVariable()
|
|
!= null) {
|
|
// Discarding LastApplError Report
|
|
} else {
|
|
if (reportListener != null) {
|
|
final Report report = processReport(decodedResponsePdu);
|
|
|
|
Thread t1 =
|
|
new Thread(
|
|
new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
reportListener.newReport(report);
|
|
}
|
|
});
|
|
t1.start();
|
|
} else {
|
|
// discarding report because no ReportListener was registered.
|
|
}
|
|
}
|
|
} else if (decodedResponsePdu.getRejectPDU() != null) {
|
|
synchronized (incomingResponses) {
|
|
if (expectedResponseId == null) {
|
|
// Discarding Reject MMS PDU because no listener for request was found.
|
|
continue;
|
|
} else if (decodedResponsePdu.getRejectPDU().getOriginalInvokeID().value.intValue()
|
|
!= expectedResponseId) {
|
|
// Discarding Reject MMS PDU because no listener with fitting invokeID was found.
|
|
continue;
|
|
} else {
|
|
try {
|
|
incomingResponses.put(decodedResponsePdu);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
}
|
|
} else if (decodedResponsePdu.getConfirmedErrorPDU() != null) {
|
|
synchronized (incomingResponses) {
|
|
if (expectedResponseId == null) {
|
|
// Discarding ConfirmedError MMS PDU because no listener for request was found.
|
|
continue;
|
|
} else if (decodedResponsePdu.getConfirmedErrorPDU().getInvokeID().value.intValue()
|
|
!= expectedResponseId) {
|
|
// Discarding ConfirmedError MMS PDU because no listener with fitting invokeID was
|
|
// found.
|
|
continue;
|
|
} else {
|
|
try {
|
|
incomingResponses.put(decodedResponsePdu);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
synchronized (incomingResponses) {
|
|
if (expectedResponseId == null) {
|
|
// Discarding ConfirmedResponse MMS PDU because no listener for request was found.
|
|
continue;
|
|
} else if (decodedResponsePdu.getConfirmedResponsePDU().getInvokeID().value.intValue()
|
|
!= expectedResponseId) {
|
|
// Discarding ConfirmedResponse MMS PDU because no listener with fitting invokeID
|
|
// was
|
|
// found.
|
|
continue;
|
|
} else {
|
|
try {
|
|
incomingResponses.put(decodedResponsePdu);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
close(e);
|
|
} catch (Exception e) {
|
|
close(new IOException("unexpected exception while receiving", e));
|
|
}
|
|
}
|
|
|
|
public void setResponseExpected(int invokeId) {
|
|
expectedResponseId = invokeId;
|
|
}
|
|
|
|
private void disconnect() {
|
|
synchronized (this) {
|
|
if (closed == false) {
|
|
closed = true;
|
|
acseAssociation.disconnect();
|
|
lastIOException = new IOException("Connection disconnected by client");
|
|
if (reportListener != null) {
|
|
Thread t1 =
|
|
new Thread(
|
|
new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
reportListener.associationClosed(lastIOException);
|
|
}
|
|
});
|
|
t1.start();
|
|
}
|
|
|
|
MMSpdu mmsPdu = new MMSpdu();
|
|
mmsPdu.setConfirmedRequestPDU(new ConfirmedRequestPDU());
|
|
try {
|
|
incomingResponses.put(mmsPdu);
|
|
} catch (InterruptedException e1) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void close(IOException e) {
|
|
synchronized (this) {
|
|
if (closed == false) {
|
|
closed = true;
|
|
acseAssociation.close();
|
|
lastIOException = e;
|
|
Thread t1 =
|
|
new Thread(
|
|
new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
reportListener.associationClosed(lastIOException);
|
|
}
|
|
});
|
|
t1.start();
|
|
|
|
MMSpdu mmsPdu = new MMSpdu();
|
|
mmsPdu.setConfirmedRequestPDU(new ConfirmedRequestPDU());
|
|
try {
|
|
incomingResponses.put(mmsPdu);
|
|
} catch (InterruptedException e1) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
IOException getLastIOException() {
|
|
return lastIOException;
|
|
}
|
|
|
|
MMSpdu removeExpectedResponse() {
|
|
synchronized (incomingResponses) {
|
|
expectedResponseId = null;
|
|
return incomingResponses.poll();
|
|
}
|
|
}
|
|
}
|
|
}
|